technical-debt-radar 1.15.1 → 1.16.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/README.md +2 -2
- package/dist/index.js +676 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Stop Node.js production crashes before merge.**
|
|
4
4
|
|
|
5
|
-
Detects event-loop blockers, dangerous ORM patterns, and architecture drift in your PRs.
|
|
5
|
+
Detects event-loop blockers, dangerous ORM patterns, security issues, and architecture drift in your PRs. 65 deterministic detection rules across 6 categories. Works with NestJS, Express, Fastify, Koa, Hapi + 7 ORMs.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ First scan is free. No account needed.
|
|
|
14
14
|
|
|
15
15
|
## What It Finds
|
|
16
16
|
|
|
17
|
-
- **Runtime risks** — sync I/O, crypto, ReDoS in request handlers (
|
|
17
|
+
- **Runtime risks** — sync I/O, crypto, child_process, ReDoS in request handlers (12 patterns)
|
|
18
18
|
- **Performance** — N+1 queries, unbounded fetches, missing pagination (9 patterns, volume-aware)
|
|
19
19
|
- **Reliability** — unhandled promises, missing timeouts, empty catches (8 patterns)
|
|
20
20
|
- **Architecture** — layer violations, circular dependencies (4 rules)
|
package/dist/index.js
CHANGED
|
@@ -39,7 +39,7 @@ var require_constants = __commonJS({
|
|
|
39
39
|
"../../packages/shared/dist/constants/index.js"(exports2) {
|
|
40
40
|
"use strict";
|
|
41
41
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
42
|
-
exports2.DEBT_DELTA_WARN_THRESHOLD = exports2.DEBT_DELTA_BLOCK_THRESHOLD = exports2.DEFAULT_LARGE_FILE_THRESHOLD = exports2.DEFAULT_COMPLEXITY_THRESHOLD = exports2.MAX_CHANGED_FILES_PER_PR = exports2.FIRST_SCAN_TIMEOUT_MS = exports2.PR_ANALYSIS_TIMEOUT_MS = exports2.MAX_AI_OUTPUT_TOKENS = exports2.MAX_AI_TOKENS_PER_FUNCTION = exports2.MAX_SUSPECT_FUNCTIONS_PER_PR = exports2.MAX_CALLER_TRACE_DEPTH = exports2.MAX_CROSS_FILE_SUSPECTS = exports2.CROSS_FILE_RULES = exports2.ARCHITECTURE_RULES = exports2.MAINTAINABILITY_RULES = exports2.PERFORMANCE_RULES = exports2.RELIABILITY_RULES = exports2.RUNTIME_RISK_RULES = exports2.VOLUME_THRESHOLDS = exports2.DEFAULT_SCORING = exports2.DEFAULT_MODE = void 0;
|
|
42
|
+
exports2.DEBT_DELTA_WARN_THRESHOLD = exports2.DEBT_DELTA_BLOCK_THRESHOLD = exports2.DEFAULT_LARGE_FILE_THRESHOLD = exports2.DEFAULT_COMPLEXITY_THRESHOLD = exports2.MAX_CHANGED_FILES_PER_PR = exports2.FIRST_SCAN_TIMEOUT_MS = exports2.PR_ANALYSIS_TIMEOUT_MS = exports2.MAX_AI_OUTPUT_TOKENS = exports2.MAX_AI_TOKENS_PER_FUNCTION = exports2.MAX_SUSPECT_FUNCTIONS_PER_PR = exports2.MAX_CALLER_TRACE_DEPTH = exports2.MAX_CROSS_FILE_SUSPECTS = exports2.SECURITY_RULES = exports2.CROSS_FILE_RULES = exports2.ARCHITECTURE_RULES = exports2.MAINTAINABILITY_RULES = exports2.PERFORMANCE_RULES = exports2.RELIABILITY_RULES = exports2.RUNTIME_RISK_RULES = exports2.VOLUME_THRESHOLDS = exports2.DEFAULT_SCORING = exports2.DEFAULT_MODE = void 0;
|
|
43
43
|
exports2.DEFAULT_MODE = "warn";
|
|
44
44
|
exports2.DEFAULT_SCORING = {
|
|
45
45
|
architecture_violation: 5,
|
|
@@ -78,7 +78,8 @@ var require_constants = __commonJS({
|
|
|
78
78
|
CPU_HEAVY_LOOP_IN_HANDLER: "cpu-heavy-loop-in-handler",
|
|
79
79
|
UNBOUNDED_ARRAY_OPERATION: "unbounded-array-operation",
|
|
80
80
|
DYNAMIC_BUFFER_ALLOC: "dynamic-buffer-alloc",
|
|
81
|
-
UNBOUNDED_PARALLEL_CALLS: "unbounded-parallel-external-calls"
|
|
81
|
+
UNBOUNDED_PARALLEL_CALLS: "unbounded-parallel-external-calls",
|
|
82
|
+
SYNC_CHILD_PROCESS: "sync-child-process"
|
|
82
83
|
};
|
|
83
84
|
exports2.RELIABILITY_RULES = {
|
|
84
85
|
UNHANDLED_PROMISE_REJECTION: "unhandled-promise-rejection",
|
|
@@ -105,7 +106,8 @@ var require_constants = __commonJS({
|
|
|
105
106
|
UNFILTERED_COUNT: "unfiltered-count-large-table",
|
|
106
107
|
RAW_SQL_NO_LIMIT: "raw-sql-no-limit",
|
|
107
108
|
RAW_SQL_UNSAFE: "raw-sql-unsafe",
|
|
108
|
-
DANGEROUS_DELETE_ALL: "dangerous-delete-all"
|
|
109
|
+
DANGEROUS_DELETE_ALL: "dangerous-delete-all",
|
|
110
|
+
RAW_SQL_TEMPLATE_INJECTION: "raw-sql-template-injection"
|
|
109
111
|
};
|
|
110
112
|
exports2.MAINTAINABILITY_RULES = {
|
|
111
113
|
HIGH_COMPLEXITY: "high-complexity",
|
|
@@ -133,7 +135,14 @@ var require_constants = __commonJS({
|
|
|
133
135
|
INDIRECT_SYNC_COMPRESSION: "indirect-sync-compression",
|
|
134
136
|
INDIRECT_BUSY_WAIT: "indirect-busy-wait",
|
|
135
137
|
INDIRECT_UNBOUNDED_JSON_PARSE: "indirect-unbounded-json-parse",
|
|
136
|
-
INDIRECT_DYNAMIC_BUFFER: "indirect-dynamic-buffer-alloc"
|
|
138
|
+
INDIRECT_DYNAMIC_BUFFER: "indirect-dynamic-buffer-alloc",
|
|
139
|
+
INDIRECT_SYNC_CHILD_PROCESS: "indirect-sync-child-process"
|
|
140
|
+
};
|
|
141
|
+
exports2.SECURITY_RULES = {
|
|
142
|
+
HARDCODED_SECRET_LITERAL: "hardcoded-secret-literal",
|
|
143
|
+
EVAL_USAGE: "eval-usage",
|
|
144
|
+
MATH_RANDOM_FOR_SECURITY: "math-random-for-security",
|
|
145
|
+
TLS_VALIDATION_DISABLED: "tls-validation-disabled"
|
|
137
146
|
};
|
|
138
147
|
exports2.MAX_CROSS_FILE_SUSPECTS = 10;
|
|
139
148
|
exports2.MAX_CALLER_TRACE_DEPTH = 2;
|
|
@@ -244,6 +253,7 @@ var require_plans = __commonJS({
|
|
|
244
253
|
githubAction: true,
|
|
245
254
|
prGate: false,
|
|
246
255
|
prCommentsDeterministic: true,
|
|
256
|
+
/* Intentional: Free users get informational PR comments (no blocking). Conversion lever for Solo upgrade. */
|
|
247
257
|
prCommentsAI: false,
|
|
248
258
|
aiScanSummary: false,
|
|
249
259
|
aiCrossFile: false,
|
|
@@ -3270,8 +3280,8 @@ var require_resolve = __commonJS({
|
|
|
3270
3280
|
}
|
|
3271
3281
|
return count;
|
|
3272
3282
|
}
|
|
3273
|
-
function getFullPath(resolver, id = "",
|
|
3274
|
-
if (
|
|
3283
|
+
function getFullPath(resolver, id = "", normalize2) {
|
|
3284
|
+
if (normalize2 !== false)
|
|
3275
3285
|
id = normalizeId(id);
|
|
3276
3286
|
const p = resolver.parse(id);
|
|
3277
3287
|
return _getFullPath(resolver, p);
|
|
@@ -4611,7 +4621,7 @@ var require_fast_uri = __commonJS({
|
|
|
4611
4621
|
"use strict";
|
|
4612
4622
|
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
|
|
4613
4623
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
4614
|
-
function
|
|
4624
|
+
function normalize2(uri, options) {
|
|
4615
4625
|
if (typeof uri === "string") {
|
|
4616
4626
|
uri = /** @type {T} */
|
|
4617
4627
|
serialize(parse(uri, options), options);
|
|
@@ -4847,7 +4857,7 @@ var require_fast_uri = __commonJS({
|
|
|
4847
4857
|
}
|
|
4848
4858
|
var fastUri = {
|
|
4849
4859
|
SCHEMES,
|
|
4850
|
-
normalize,
|
|
4860
|
+
normalize: normalize2,
|
|
4851
4861
|
resolve: resolve7,
|
|
4852
4862
|
resolveComponent,
|
|
4853
4863
|
equal,
|
|
@@ -9941,6 +9951,7 @@ var require_shared_rules = __commonJS({
|
|
|
9941
9951
|
"sync-fs-in-handler",
|
|
9942
9952
|
"sync-crypto",
|
|
9943
9953
|
"sync-compression",
|
|
9954
|
+
"sync-child-process",
|
|
9944
9955
|
"redos-vulnerable-regex",
|
|
9945
9956
|
"busy-wait-loop",
|
|
9946
9957
|
"unhandled-promise"
|
|
@@ -14088,6 +14099,7 @@ var require_runtime_risk_detector = __commonJS({
|
|
|
14088
14099
|
detectSyncFs(sourceFile, file.path, handlers, allFunctions, policy, violations, unflaggedPatterns);
|
|
14089
14100
|
detectSyncCrypto(sourceFile, file.path, handlers, allFunctions, policy, violations, unflaggedPatterns);
|
|
14090
14101
|
detectSyncCompression(sourceFile, file.path, handlers, allFunctions, policy, violations, unflaggedPatterns);
|
|
14102
|
+
detectSyncChildProcess(sourceFile, file.path, handlers, allFunctions, policy, violations, unflaggedPatterns);
|
|
14091
14103
|
detectRedosRegex(sourceFile, file.path, allFunctions, policy, violations);
|
|
14092
14104
|
detectBusyWaitLoop(sourceFile, file.path, handlers, allFunctions, policy, violations, unflaggedPatterns);
|
|
14093
14105
|
detectUnhandledPromise(sourceFile, file.path, allFunctions, policy, violations);
|
|
@@ -14436,6 +14448,30 @@ var require_runtime_risk_detector = __commonJS({
|
|
|
14436
14448
|
}
|
|
14437
14449
|
});
|
|
14438
14450
|
}
|
|
14451
|
+
var SYNC_CHILD_PROCESS_METHODS = /* @__PURE__ */ new Set([
|
|
14452
|
+
"execSync",
|
|
14453
|
+
"spawnSync",
|
|
14454
|
+
"execFileSync"
|
|
14455
|
+
]);
|
|
14456
|
+
function detectSyncChildProcess(sourceFile, filePath, handlers, allFunctions, policy, violations, unflaggedPatterns) {
|
|
14457
|
+
sourceFile.forEachDescendant((node) => {
|
|
14458
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
14459
|
+
return;
|
|
14460
|
+
const expr = node.getExpression();
|
|
14461
|
+
const methodName = ts_morph_1.Node.isPropertyAccessExpression(expr) ? expr.getName() : ts_morph_1.Node.isIdentifier(expr) ? expr.getText() : null;
|
|
14462
|
+
if (methodName && SYNC_CHILD_PROCESS_METHODS.has(methodName)) {
|
|
14463
|
+
const handler = isInsideHandler(node, handlers);
|
|
14464
|
+
if (handler) {
|
|
14465
|
+
violations.push(makeViolation(shared_1.RUNTIME_RISK_RULES.SYNC_CHILD_PROCESS, filePath, node.getStartLineNumber(), `Synchronous child process '${methodName}' inside handler '${handler.name}' blocks the event loop`, policy, handler.name, `Use the async equivalent \u2014 child_process.${methodName.replace("Sync", "")}() with a Promise, or spawn/execFile`));
|
|
14466
|
+
} else {
|
|
14467
|
+
const enclosingFn = getEnclosingFunction(node, allFunctions);
|
|
14468
|
+
if (enclosingFn && enclosingFn.name !== "<anonymous>") {
|
|
14469
|
+
unflaggedPatterns.push(makeUnflagged(shared_1.RUNTIME_RISK_RULES.SYNC_CHILD_PROCESS, filePath, node.getStartLineNumber(), enclosingFn.name, methodName, `Synchronous child process '${methodName}' in '${enclosingFn.name}' (not in handler scope)`, extractSnippet(node, sourceFile)));
|
|
14470
|
+
}
|
|
14471
|
+
}
|
|
14472
|
+
}
|
|
14473
|
+
});
|
|
14474
|
+
}
|
|
14439
14475
|
function detectRedosRegex(sourceFile, filePath, allFunctions, policy, violations) {
|
|
14440
14476
|
sourceFile.forEachDescendant((node) => {
|
|
14441
14477
|
if (!ts_morph_1.Node.isRegularExpressionLiteral(node))
|
|
@@ -16467,6 +16503,7 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
16467
16503
|
"$executeRawUnsafe"
|
|
16468
16504
|
]);
|
|
16469
16505
|
var TYPEORM_RAW_SQL_METHODS = /* @__PURE__ */ new Set(["query"]);
|
|
16506
|
+
var SQL_BOUNDING_CLAUSE_REGEX = /\bLIMIT\b|\bTOP\s*\(?\s*(?:\d+|@\w+|:\w+|\$\d+|\?)|\bFETCH\s+(?:NEXT|FIRST)\s+(?:\d+|@\w+|\?)\s+ROWS?\s+ONLY\b|\bROWNUM\s*<=?\s*\d/i;
|
|
16470
16507
|
function detectRawSqlNoLimit(sourceFile, filePath, fns, policy, violations) {
|
|
16471
16508
|
sourceFile.forEachDescendant((node) => {
|
|
16472
16509
|
if (ts_morph_1.Node.isCallExpression(node)) {
|
|
@@ -16506,7 +16543,27 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
16506
16543
|
const firstArg = args[0];
|
|
16507
16544
|
if (ts_morph_1.Node.isStringLiteral(firstArg)) {
|
|
16508
16545
|
sqlText = firstArg.getLiteralValue();
|
|
16509
|
-
} else if (ts_morph_1.Node.isTemplateExpression(firstArg)
|
|
16546
|
+
} else if (ts_morph_1.Node.isTemplateExpression(firstArg)) {
|
|
16547
|
+
sqlText = firstArg.getText();
|
|
16548
|
+
if (firstArg.getTemplateSpans().length > 0) {
|
|
16549
|
+
const tplFn = getEnclosingFn(node, fns);
|
|
16550
|
+
violations.push({
|
|
16551
|
+
category: "performance",
|
|
16552
|
+
type: shared_1.PERFORMANCE_RULES.RAW_SQL_TEMPLATE_INJECTION,
|
|
16553
|
+
ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_TEMPLATE_INJECTION,
|
|
16554
|
+
severity: "critical",
|
|
16555
|
+
source: "deterministic",
|
|
16556
|
+
confidence: "high",
|
|
16557
|
+
file: filePath,
|
|
16558
|
+
line: node.getStartLineNumber(),
|
|
16559
|
+
function: tplFn?.name,
|
|
16560
|
+
message: `${method}() with template literal interpolation \u2014 SQL injection risk. Interpolated values become executable SQL syntax.`,
|
|
16561
|
+
suggestion: "Use parameterized queries: pass values as bound parameters (e.g. $1, @param, ?) instead of interpolating them into the SQL string.",
|
|
16562
|
+
debtPoints: policy.scoring.performance_risk_critical,
|
|
16563
|
+
gateAction: "block"
|
|
16564
|
+
});
|
|
16565
|
+
}
|
|
16566
|
+
} else if (ts_morph_1.Node.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
16510
16567
|
sqlText = firstArg.getText();
|
|
16511
16568
|
} else if (ts_morph_1.Node.isBinaryExpression(firstArg)) {
|
|
16512
16569
|
sqlText = firstArg.getText();
|
|
@@ -16584,8 +16641,8 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
16584
16641
|
}
|
|
16585
16642
|
return;
|
|
16586
16643
|
}
|
|
16587
|
-
const
|
|
16588
|
-
if (
|
|
16644
|
+
const hasBoundingClause = SQL_BOUNDING_CLAUSE_REGEX.test(sqlText);
|
|
16645
|
+
if (hasBoundingClause && !isUnsafe)
|
|
16589
16646
|
return;
|
|
16590
16647
|
if (/\b(COUNT|SUM|AVG|MIN|MAX)\s*\(/i.test(sqlText) && !isUnsafe)
|
|
16591
16648
|
return;
|
|
@@ -16609,8 +16666,8 @@ var require_perf_pattern_detector = __commonJS({
|
|
|
16609
16666
|
debtPoints: policy.scoring.performance_risk_critical,
|
|
16610
16667
|
gateAction: "block"
|
|
16611
16668
|
});
|
|
16612
|
-
if (!
|
|
16613
|
-
pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT, filePath, line, `Raw SQL SELECT without LIMIT${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may return unbounded results`, policy, vol, fn?.name, "Add LIMIT clause to the SQL query"));
|
|
16669
|
+
if (!hasBoundingClause) {
|
|
16670
|
+
pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT, filePath, line, `Raw SQL SELECT without a row-limiting clause (LIMIT / TOP / FETCH)${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may return unbounded results`, policy, vol, fn?.name, "Add LIMIT clause to the SQL query"));
|
|
16614
16671
|
}
|
|
16615
16672
|
} else {
|
|
16616
16673
|
pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT, filePath, line, `Raw SQL SELECT without LIMIT${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may return unbounded results`, policy, vol, fn?.name, "Add LIMIT clause to the SQL query"));
|
|
@@ -18667,6 +18724,7 @@ var require_cross_file_analyzer = __commonJS({
|
|
|
18667
18724
|
"sync-fs-in-handler": "indirect-sync-fs",
|
|
18668
18725
|
"sync-crypto": "indirect-sync-crypto",
|
|
18669
18726
|
"sync-compression": "indirect-sync-compression",
|
|
18727
|
+
"sync-child-process": "indirect-sync-child-process",
|
|
18670
18728
|
"busy-wait-loop": "indirect-busy-wait",
|
|
18671
18729
|
"unbounded-json-parse": "indirect-unbounded-json-parse",
|
|
18672
18730
|
"dynamic-buffer-alloc": "indirect-dynamic-buffer-alloc"
|
|
@@ -19883,6 +19941,437 @@ var require_coverage_delta_detector = __commonJS({
|
|
|
19883
19941
|
}
|
|
19884
19942
|
});
|
|
19885
19943
|
|
|
19944
|
+
// ../../packages/analyzers/dist/typescript/secret-scanner.js
|
|
19945
|
+
var require_secret_scanner = __commonJS({
|
|
19946
|
+
"../../packages/analyzers/dist/typescript/secret-scanner.js"(exports2) {
|
|
19947
|
+
"use strict";
|
|
19948
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
19949
|
+
exports2.detectHardcodedSecrets = detectHardcodedSecrets;
|
|
19950
|
+
var shared_1 = require_dist();
|
|
19951
|
+
var ts_morph_1 = require("ts-morph");
|
|
19952
|
+
var SECRET_PATTERNS = [
|
|
19953
|
+
{ id: "npm", name: "npm access token", regex: /\bnpm_[A-Za-z0-9]{36,}\b/ },
|
|
19954
|
+
{ id: "stripe-live", name: "Stripe live secret key", regex: /\bsk_live_[A-Za-z0-9]{24,}\b/ },
|
|
19955
|
+
{ id: "stripe-test", name: "Stripe test secret key", regex: /\bsk_test_[A-Za-z0-9]{24,}\b/ },
|
|
19956
|
+
{ id: "github-pat", name: "GitHub personal access token", regex: /\bghp_[A-Za-z0-9]{36}\b/ },
|
|
19957
|
+
{ id: "github-app", name: "GitHub App token", regex: /\bghs_[A-Za-z0-9]{36}\b/ },
|
|
19958
|
+
{ id: "github-oauth", name: "GitHub OAuth token", regex: /\bgho_[A-Za-z0-9]{36}\b/ },
|
|
19959
|
+
{ id: "paddle-live", name: "Paddle live API key", regex: /\bpdl_live_apikey_[A-Za-z0-9_]{8,}/ },
|
|
19960
|
+
{ id: "paddle-sandbox", name: "Paddle sandbox API key", regex: /\bpdl_sdbx_apikey_[A-Za-z0-9_]{8,}/ },
|
|
19961
|
+
{ id: "aws-access-key", name: "AWS access key ID", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
19962
|
+
{ id: "google-api", name: "Google API key", regex: /\bAIza[A-Za-z0-9_-]{35}\b/ },
|
|
19963
|
+
{ id: "slack", name: "Slack token", regex: /\bxox[bpsa]-\d+-\d+-[A-Za-z0-9]+\b/ },
|
|
19964
|
+
{ id: "anthropic", name: "Anthropic API key", regex: /\bsk-ant-[A-Za-z0-9-]{32,}\b/ },
|
|
19965
|
+
{ id: "openai", name: "OpenAI API key", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/ },
|
|
19966
|
+
{ id: "resend", name: "Resend API key", regex: /\bre_[A-Za-z0-9_]{20,}\b/ },
|
|
19967
|
+
{ id: "jwt", name: "JSON Web Token", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
|
19968
|
+
{ id: "private-key", name: "private key (PEM block)", regex: /-----BEGIN (?:(?:RSA|EC|OPENSSH|DSA|PGP) )?PRIVATE KEY-----/ }
|
|
19969
|
+
];
|
|
19970
|
+
var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.fixture\.|\.example\.|\.stories\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|__fixtures__|test|tests|e2e|fixtures|examples|mocks|validation)(\/|$)/i;
|
|
19971
|
+
var PLACEHOLDER_SUBSTRINGS = [
|
|
19972
|
+
"placeholder",
|
|
19973
|
+
"example",
|
|
19974
|
+
"dummy",
|
|
19975
|
+
"fake",
|
|
19976
|
+
"mock",
|
|
19977
|
+
"your-",
|
|
19978
|
+
"xxx",
|
|
19979
|
+
"redacted"
|
|
19980
|
+
];
|
|
19981
|
+
var DOC_PROPERTY_KEYS = /* @__PURE__ */ new Set([
|
|
19982
|
+
"example",
|
|
19983
|
+
"placeholder",
|
|
19984
|
+
"description",
|
|
19985
|
+
"comment",
|
|
19986
|
+
"docexample",
|
|
19987
|
+
"sample"
|
|
19988
|
+
]);
|
|
19989
|
+
async function detectHardcodedSecrets(input, policy) {
|
|
19990
|
+
const violations = [];
|
|
19991
|
+
const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
|
|
19992
|
+
for (const file of input.changedFiles) {
|
|
19993
|
+
if (file.status === "deleted")
|
|
19994
|
+
continue;
|
|
19995
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
|
|
19996
|
+
continue;
|
|
19997
|
+
if (EXCLUDED_PATH_REGEX.test(file.path))
|
|
19998
|
+
continue;
|
|
19999
|
+
const sourceFile = project.createSourceFile(file.path, file.content);
|
|
20000
|
+
scanFile(sourceFile, file.path, policy, violations);
|
|
20001
|
+
project.removeSourceFile(sourceFile);
|
|
20002
|
+
}
|
|
20003
|
+
return violations;
|
|
20004
|
+
}
|
|
20005
|
+
function scanFile(sourceFile, filePath, policy, violations) {
|
|
20006
|
+
sourceFile.forEachDescendant((node) => {
|
|
20007
|
+
let value;
|
|
20008
|
+
if (ts_morph_1.Node.isStringLiteral(node)) {
|
|
20009
|
+
value = node.getLiteralValue();
|
|
20010
|
+
} else if (ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
20011
|
+
value = node.getLiteralValue();
|
|
20012
|
+
} else {
|
|
20013
|
+
return;
|
|
20014
|
+
}
|
|
20015
|
+
if (!value || value.length < 12)
|
|
20016
|
+
return;
|
|
20017
|
+
const lower = value.toLowerCase();
|
|
20018
|
+
if (PLACEHOLDER_SUBSTRINGS.some((s) => lower.includes(s)))
|
|
20019
|
+
return;
|
|
20020
|
+
if (isDocumentationValue(node))
|
|
20021
|
+
return;
|
|
20022
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
20023
|
+
if (pattern.regex.test(value)) {
|
|
20024
|
+
violations.push({
|
|
20025
|
+
category: "runtime_risk",
|
|
20026
|
+
type: shared_1.SECURITY_RULES.HARDCODED_SECRET_LITERAL,
|
|
20027
|
+
ruleId: shared_1.SECURITY_RULES.HARDCODED_SECRET_LITERAL,
|
|
20028
|
+
severity: "critical",
|
|
20029
|
+
source: "deterministic",
|
|
20030
|
+
confidence: "high",
|
|
20031
|
+
file: filePath,
|
|
20032
|
+
line: node.getStartLineNumber(),
|
|
20033
|
+
message: `Hardcoded ${pattern.name} found in source \u2014 credentials must never be committed to version control.`,
|
|
20034
|
+
suggestion: "Move the value to an environment variable or a secret manager, and rotate the exposed credential immediately.",
|
|
20035
|
+
debtPoints: policy.scoring.runtime_risk_critical,
|
|
20036
|
+
gateAction: "block"
|
|
20037
|
+
});
|
|
20038
|
+
break;
|
|
20039
|
+
}
|
|
20040
|
+
}
|
|
20041
|
+
});
|
|
20042
|
+
}
|
|
20043
|
+
function isDocumentationValue(node) {
|
|
20044
|
+
const parent = node.getParent();
|
|
20045
|
+
if (parent && ts_morph_1.Node.isPropertyAssignment(parent)) {
|
|
20046
|
+
const name = parent.getName().toLowerCase().replace(/['"`]/g, "");
|
|
20047
|
+
if (DOC_PROPERTY_KEYS.has(name))
|
|
20048
|
+
return true;
|
|
20049
|
+
}
|
|
20050
|
+
return false;
|
|
20051
|
+
}
|
|
20052
|
+
}
|
|
20053
|
+
});
|
|
20054
|
+
|
|
20055
|
+
// ../../packages/analyzers/dist/typescript/eval-detector.js
|
|
20056
|
+
var require_eval_detector = __commonJS({
|
|
20057
|
+
"../../packages/analyzers/dist/typescript/eval-detector.js"(exports2) {
|
|
20058
|
+
"use strict";
|
|
20059
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
20060
|
+
exports2.detectEvalUsage = detectEvalUsage;
|
|
20061
|
+
var shared_1 = require_dist();
|
|
20062
|
+
var ts_morph_1 = require("ts-morph");
|
|
20063
|
+
async function detectEvalUsage(input, policy) {
|
|
20064
|
+
const violations = [];
|
|
20065
|
+
const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
|
|
20066
|
+
for (const file of input.changedFiles) {
|
|
20067
|
+
if (file.status === "deleted")
|
|
20068
|
+
continue;
|
|
20069
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
|
|
20070
|
+
continue;
|
|
20071
|
+
const sourceFile = project.createSourceFile(file.path, file.content);
|
|
20072
|
+
scanFile(sourceFile, file.path, policy, violations);
|
|
20073
|
+
project.removeSourceFile(sourceFile);
|
|
20074
|
+
}
|
|
20075
|
+
return violations;
|
|
20076
|
+
}
|
|
20077
|
+
function makeViolation(file, line, message, suggestion, policy) {
|
|
20078
|
+
return {
|
|
20079
|
+
category: "runtime_risk",
|
|
20080
|
+
type: shared_1.SECURITY_RULES.EVAL_USAGE,
|
|
20081
|
+
ruleId: shared_1.SECURITY_RULES.EVAL_USAGE,
|
|
20082
|
+
severity: "critical",
|
|
20083
|
+
source: "deterministic",
|
|
20084
|
+
confidence: "high",
|
|
20085
|
+
file,
|
|
20086
|
+
line,
|
|
20087
|
+
message,
|
|
20088
|
+
suggestion,
|
|
20089
|
+
debtPoints: policy.scoring.runtime_risk_critical,
|
|
20090
|
+
gateAction: "block"
|
|
20091
|
+
};
|
|
20092
|
+
}
|
|
20093
|
+
function scanFile(sourceFile, filePath, policy, violations) {
|
|
20094
|
+
sourceFile.forEachDescendant((node) => {
|
|
20095
|
+
if (ts_morph_1.Node.isCallExpression(node)) {
|
|
20096
|
+
const expr = node.getExpression();
|
|
20097
|
+
if (ts_morph_1.Node.isIdentifier(expr) && expr.getText() === "eval") {
|
|
20098
|
+
violations.push(makeViolation(filePath, node.getStartLineNumber(), "eval() executes its string argument as code \u2014 an arbitrary code execution vector.", "Remove eval(): parse data with JSON.parse(), use a lookup object/map, or refactor to avoid dynamic code.", policy));
|
|
20099
|
+
return;
|
|
20100
|
+
}
|
|
20101
|
+
const calleeName = ts_morph_1.Node.isIdentifier(expr) ? expr.getText() : ts_morph_1.Node.isPropertyAccessExpression(expr) ? expr.getName() : "";
|
|
20102
|
+
if (calleeName === "setTimeout" || calleeName === "setInterval") {
|
|
20103
|
+
const args = node.getArguments();
|
|
20104
|
+
if (args.length > 0 && isStringArgument(args[0])) {
|
|
20105
|
+
violations.push(makeViolation(filePath, node.getStartLineNumber(), `${calleeName}() called with a string argument \u2014 the string is evaluated as code.`, `Pass a function instead: ${calleeName}(() => { /* ... */ }, delay).`, policy));
|
|
20106
|
+
}
|
|
20107
|
+
return;
|
|
20108
|
+
}
|
|
20109
|
+
}
|
|
20110
|
+
if (ts_morph_1.Node.isNewExpression(node)) {
|
|
20111
|
+
const expr = node.getExpression();
|
|
20112
|
+
if (ts_morph_1.Node.isIdentifier(expr) && expr.getText() === "Function") {
|
|
20113
|
+
const args = node.getArguments();
|
|
20114
|
+
if (args.length >= 1) {
|
|
20115
|
+
violations.push(makeViolation(filePath, node.getStartLineNumber(), "new Function() compiles a function from a string \u2014 equivalent to eval(), an arbitrary code execution vector.", "Refactor to avoid building code at runtime \u2014 use a closure, a lookup table, or a real parser.", policy));
|
|
20116
|
+
}
|
|
20117
|
+
return;
|
|
20118
|
+
}
|
|
20119
|
+
}
|
|
20120
|
+
});
|
|
20121
|
+
}
|
|
20122
|
+
function isStringArgument(node) {
|
|
20123
|
+
return ts_morph_1.Node.isStringLiteral(node) || ts_morph_1.Node.isTemplateExpression(node) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node);
|
|
20124
|
+
}
|
|
20125
|
+
}
|
|
20126
|
+
});
|
|
20127
|
+
|
|
20128
|
+
// ../../packages/analyzers/dist/typescript/math-random-detector.js
|
|
20129
|
+
var require_math_random_detector = __commonJS({
|
|
20130
|
+
"../../packages/analyzers/dist/typescript/math-random-detector.js"(exports2) {
|
|
20131
|
+
"use strict";
|
|
20132
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
20133
|
+
exports2.detectMathRandomForSecurity = detectMathRandomForSecurity;
|
|
20134
|
+
var shared_1 = require_dist();
|
|
20135
|
+
var ts_morph_1 = require("ts-morph");
|
|
20136
|
+
var SECURITY_KEYWORDS = [
|
|
20137
|
+
"token",
|
|
20138
|
+
"secret",
|
|
20139
|
+
"password",
|
|
20140
|
+
"passwd",
|
|
20141
|
+
"apikey",
|
|
20142
|
+
"api_key",
|
|
20143
|
+
"nonce",
|
|
20144
|
+
"salt",
|
|
20145
|
+
"csrf",
|
|
20146
|
+
"otp",
|
|
20147
|
+
"verifier",
|
|
20148
|
+
"sessionid",
|
|
20149
|
+
"session_id",
|
|
20150
|
+
"session",
|
|
20151
|
+
"credential",
|
|
20152
|
+
"privatekey"
|
|
20153
|
+
];
|
|
20154
|
+
var NON_SECURITY_KEYWORDS = [
|
|
20155
|
+
"color",
|
|
20156
|
+
"colour",
|
|
20157
|
+
"position",
|
|
20158
|
+
"rotation",
|
|
20159
|
+
"delay",
|
|
20160
|
+
"jitter",
|
|
20161
|
+
"sample",
|
|
20162
|
+
"index",
|
|
20163
|
+
"angle",
|
|
20164
|
+
"opacity",
|
|
20165
|
+
"offset",
|
|
20166
|
+
"hue",
|
|
20167
|
+
"duration",
|
|
20168
|
+
"mock",
|
|
20169
|
+
"fake",
|
|
20170
|
+
"dummy",
|
|
20171
|
+
"demo"
|
|
20172
|
+
];
|
|
20173
|
+
var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.fixture\.|\.example\.|\.stories\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|test|tests|e2e|mocks)(\/|$)/i;
|
|
20174
|
+
async function detectMathRandomForSecurity(input, policy) {
|
|
20175
|
+
const violations = [];
|
|
20176
|
+
const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
|
|
20177
|
+
for (const file of input.changedFiles) {
|
|
20178
|
+
if (file.status === "deleted")
|
|
20179
|
+
continue;
|
|
20180
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
|
|
20181
|
+
continue;
|
|
20182
|
+
if (EXCLUDED_PATH_REGEX.test(file.path))
|
|
20183
|
+
continue;
|
|
20184
|
+
const sourceFile = project.createSourceFile(file.path, file.content);
|
|
20185
|
+
scanFile(sourceFile, file.path, policy, violations);
|
|
20186
|
+
project.removeSourceFile(sourceFile);
|
|
20187
|
+
}
|
|
20188
|
+
return violations;
|
|
20189
|
+
}
|
|
20190
|
+
function scanFile(sourceFile, filePath, policy, violations) {
|
|
20191
|
+
sourceFile.forEachDescendant((node) => {
|
|
20192
|
+
if (!ts_morph_1.Node.isCallExpression(node))
|
|
20193
|
+
return;
|
|
20194
|
+
const expr = node.getExpression();
|
|
20195
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
20196
|
+
return;
|
|
20197
|
+
if (expr.getName() !== "random")
|
|
20198
|
+
return;
|
|
20199
|
+
const obj = expr.getExpression();
|
|
20200
|
+
if (!ts_morph_1.Node.isIdentifier(obj) || obj.getText() !== "Math")
|
|
20201
|
+
return;
|
|
20202
|
+
const contextName = findContextName(node);
|
|
20203
|
+
if (!contextName || !isSecurityContext(contextName))
|
|
20204
|
+
return;
|
|
20205
|
+
violations.push({
|
|
20206
|
+
category: "runtime_risk",
|
|
20207
|
+
type: shared_1.SECURITY_RULES.MATH_RANDOM_FOR_SECURITY,
|
|
20208
|
+
ruleId: shared_1.SECURITY_RULES.MATH_RANDOM_FOR_SECURITY,
|
|
20209
|
+
severity: "warning",
|
|
20210
|
+
source: "deterministic",
|
|
20211
|
+
confidence: "medium",
|
|
20212
|
+
file: filePath,
|
|
20213
|
+
line: node.getStartLineNumber(),
|
|
20214
|
+
message: `Math.random() used for security-sensitive value '${contextName}' \u2014 its output is predictable, not cryptographically secure.`,
|
|
20215
|
+
suggestion: "Use crypto.randomBytes() / crypto.randomUUID() (Node) or crypto.getRandomValues() (Web) for tokens, secrets, and IDs.",
|
|
20216
|
+
debtPoints: policy.scoring.runtime_risk_warning,
|
|
20217
|
+
gateAction: "warn"
|
|
20218
|
+
});
|
|
20219
|
+
});
|
|
20220
|
+
}
|
|
20221
|
+
function findContextName(randomCall) {
|
|
20222
|
+
let current = randomCall;
|
|
20223
|
+
for (let depth = 0; depth < 15; depth++) {
|
|
20224
|
+
const parent = current.getParent();
|
|
20225
|
+
if (!parent)
|
|
20226
|
+
return void 0;
|
|
20227
|
+
if (ts_morph_1.Node.isVariableDeclaration(parent)) {
|
|
20228
|
+
return parent.getName();
|
|
20229
|
+
}
|
|
20230
|
+
if (ts_morph_1.Node.isPropertyAssignment(parent) || ts_morph_1.Node.isShorthandPropertyAssignment(parent)) {
|
|
20231
|
+
return parent.getName().replace(/['"`]/g, "");
|
|
20232
|
+
}
|
|
20233
|
+
if (ts_morph_1.Node.isPropertyDeclaration(parent)) {
|
|
20234
|
+
return parent.getName();
|
|
20235
|
+
}
|
|
20236
|
+
if (ts_morph_1.Node.isBinaryExpression(parent)) {
|
|
20237
|
+
const op = parent.getOperatorToken().getText();
|
|
20238
|
+
if (op === "=" || op === "+=") {
|
|
20239
|
+
return lastSegment(parent.getLeft().getText());
|
|
20240
|
+
}
|
|
20241
|
+
current = parent;
|
|
20242
|
+
continue;
|
|
20243
|
+
}
|
|
20244
|
+
if (ts_morph_1.Node.isReturnStatement(parent)) {
|
|
20245
|
+
const fn = parent.getFirstAncestor((a) => ts_morph_1.Node.isFunctionDeclaration(a) || ts_morph_1.Node.isMethodDeclaration(a));
|
|
20246
|
+
if (fn && (ts_morph_1.Node.isFunctionDeclaration(fn) || ts_morph_1.Node.isMethodDeclaration(fn))) {
|
|
20247
|
+
return fn.getName() ?? void 0;
|
|
20248
|
+
}
|
|
20249
|
+
return void 0;
|
|
20250
|
+
}
|
|
20251
|
+
if (ts_morph_1.Node.isCallExpression(parent)) {
|
|
20252
|
+
if (parent.getArguments().some((a) => a === current))
|
|
20253
|
+
return void 0;
|
|
20254
|
+
current = parent;
|
|
20255
|
+
continue;
|
|
20256
|
+
}
|
|
20257
|
+
if (ts_morph_1.Node.isParenthesizedExpression(parent) || ts_morph_1.Node.isPropertyAccessExpression(parent) || ts_morph_1.Node.isAwaitExpression(parent) || ts_morph_1.Node.isAsExpression(parent) || ts_morph_1.Node.isTemplateSpan(parent) || ts_morph_1.Node.isTemplateExpression(parent) || ts_morph_1.Node.isNonNullExpression(parent)) {
|
|
20258
|
+
current = parent;
|
|
20259
|
+
continue;
|
|
20260
|
+
}
|
|
20261
|
+
return void 0;
|
|
20262
|
+
}
|
|
20263
|
+
return void 0;
|
|
20264
|
+
}
|
|
20265
|
+
function lastSegment(text) {
|
|
20266
|
+
const parts = text.split(".");
|
|
20267
|
+
return parts[parts.length - 1] ?? text;
|
|
20268
|
+
}
|
|
20269
|
+
function isSecurityContext(name) {
|
|
20270
|
+
const n = name.toLowerCase();
|
|
20271
|
+
if (NON_SECURITY_KEYWORDS.some((k) => n.includes(k)))
|
|
20272
|
+
return false;
|
|
20273
|
+
return SECURITY_KEYWORDS.some((k) => n.includes(k));
|
|
20274
|
+
}
|
|
20275
|
+
}
|
|
20276
|
+
});
|
|
20277
|
+
|
|
20278
|
+
// ../../packages/analyzers/dist/typescript/tls-detector.js
|
|
20279
|
+
var require_tls_detector = __commonJS({
|
|
20280
|
+
"../../packages/analyzers/dist/typescript/tls-detector.js"(exports2) {
|
|
20281
|
+
"use strict";
|
|
20282
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
20283
|
+
exports2.detectTlsValidationDisabled = detectTlsValidationDisabled;
|
|
20284
|
+
var shared_1 = require_dist();
|
|
20285
|
+
var ts_morph_1 = require("ts-morph");
|
|
20286
|
+
var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.fixture\.|\.example\.|\.stories\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|test|tests|e2e|mocks)(\/|$)/i;
|
|
20287
|
+
var ALLOW_COMMENT_REGEX = /\b(dev-only|local-only|allow-insecure)\b/i;
|
|
20288
|
+
async function detectTlsValidationDisabled(input, policy) {
|
|
20289
|
+
const violations = [];
|
|
20290
|
+
const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
|
|
20291
|
+
for (const file of input.changedFiles) {
|
|
20292
|
+
if (file.status === "deleted")
|
|
20293
|
+
continue;
|
|
20294
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
|
|
20295
|
+
continue;
|
|
20296
|
+
if (EXCLUDED_PATH_REGEX.test(file.path))
|
|
20297
|
+
continue;
|
|
20298
|
+
const sourceFile = project.createSourceFile(file.path, file.content);
|
|
20299
|
+
scanFile(sourceFile, file.path, policy, violations);
|
|
20300
|
+
project.removeSourceFile(sourceFile);
|
|
20301
|
+
}
|
|
20302
|
+
return violations;
|
|
20303
|
+
}
|
|
20304
|
+
function scanFile(sourceFile, filePath, policy, violations) {
|
|
20305
|
+
sourceFile.forEachDescendant((node) => {
|
|
20306
|
+
if (!ts_morph_1.Node.isPropertyAssignment(node))
|
|
20307
|
+
return;
|
|
20308
|
+
const name = node.getName().replace(/['"`]/g, "");
|
|
20309
|
+
const init = node.getInitializer();
|
|
20310
|
+
if (!init)
|
|
20311
|
+
return;
|
|
20312
|
+
let problem;
|
|
20313
|
+
if ((name === "rejectUnauthorized" || name === "strictSSL") && init.getText().trim() === "false") {
|
|
20314
|
+
problem = `${name}: false disables TLS certificate verification`;
|
|
20315
|
+
} else if (name === "checkServerIdentity" && isNoOpFunction(init)) {
|
|
20316
|
+
problem = "checkServerIdentity is overridden with a no-op \u2014 TLS hostname verification is disabled";
|
|
20317
|
+
}
|
|
20318
|
+
if (!problem)
|
|
20319
|
+
return;
|
|
20320
|
+
if (hasAllowComment(node, sourceFile))
|
|
20321
|
+
return;
|
|
20322
|
+
violations.push({
|
|
20323
|
+
category: "runtime_risk",
|
|
20324
|
+
type: shared_1.SECURITY_RULES.TLS_VALIDATION_DISABLED,
|
|
20325
|
+
ruleId: shared_1.SECURITY_RULES.TLS_VALIDATION_DISABLED,
|
|
20326
|
+
severity: "warning",
|
|
20327
|
+
source: "deterministic",
|
|
20328
|
+
confidence: "high",
|
|
20329
|
+
file: filePath,
|
|
20330
|
+
line: node.getStartLineNumber(),
|
|
20331
|
+
function: getEnclosingFunctionName(node),
|
|
20332
|
+
message: `${problem} \u2014 the connection becomes vulnerable to man-in-the-middle attacks.`,
|
|
20333
|
+
suggestion: "Keep TLS verification enabled. For a self-signed certificate, register its CA via the `ca` option instead of disabling verification.",
|
|
20334
|
+
debtPoints: policy.scoring.runtime_risk_warning,
|
|
20335
|
+
gateAction: "warn"
|
|
20336
|
+
});
|
|
20337
|
+
});
|
|
20338
|
+
}
|
|
20339
|
+
function isNoOpFunction(node) {
|
|
20340
|
+
if (!ts_morph_1.Node.isArrowFunction(node) && !ts_morph_1.Node.isFunctionExpression(node))
|
|
20341
|
+
return false;
|
|
20342
|
+
const body = node.getBody();
|
|
20343
|
+
if (!body)
|
|
20344
|
+
return false;
|
|
20345
|
+
if (!ts_morph_1.Node.isBlock(body)) {
|
|
20346
|
+
const text = body.getText().trim();
|
|
20347
|
+
return text === "undefined" || text === "null" || text === "void 0";
|
|
20348
|
+
}
|
|
20349
|
+
const statements = body.getStatements();
|
|
20350
|
+
if (statements.length === 0)
|
|
20351
|
+
return true;
|
|
20352
|
+
if (statements.length === 1 && ts_morph_1.Node.isReturnStatement(statements[0])) {
|
|
20353
|
+
const returned = statements[0].getExpression()?.getText().trim() ?? "";
|
|
20354
|
+
return returned === "" || returned === "undefined" || returned === "null" || returned === "void 0";
|
|
20355
|
+
}
|
|
20356
|
+
return false;
|
|
20357
|
+
}
|
|
20358
|
+
function hasAllowComment(node, sourceFile) {
|
|
20359
|
+
const leading = node.getLeadingCommentRanges().map((r) => r.getText()).join(" ");
|
|
20360
|
+
if (ALLOW_COMMENT_REGEX.test(leading))
|
|
20361
|
+
return true;
|
|
20362
|
+
const lineText = sourceFile.getFullText().split("\n")[node.getStartLineNumber() - 1] ?? "";
|
|
20363
|
+
return ALLOW_COMMENT_REGEX.test(lineText);
|
|
20364
|
+
}
|
|
20365
|
+
function getEnclosingFunctionName(node) {
|
|
20366
|
+
const fn = node.getFirstAncestor((a) => ts_morph_1.Node.isFunctionDeclaration(a) || ts_morph_1.Node.isMethodDeclaration(a));
|
|
20367
|
+
if (fn && (ts_morph_1.Node.isFunctionDeclaration(fn) || ts_morph_1.Node.isMethodDeclaration(fn))) {
|
|
20368
|
+
return fn.getName() ?? void 0;
|
|
20369
|
+
}
|
|
20370
|
+
return void 0;
|
|
20371
|
+
}
|
|
20372
|
+
}
|
|
20373
|
+
});
|
|
20374
|
+
|
|
19886
20375
|
// ../../packages/analyzers/dist/typescript/orchestrator.js
|
|
19887
20376
|
var require_orchestrator = __commonJS({
|
|
19888
20377
|
"../../packages/analyzers/dist/typescript/orchestrator.js"(exports2) {
|
|
@@ -19940,6 +20429,10 @@ var require_orchestrator = __commonJS({
|
|
|
19940
20429
|
var missing_tests_detector_1 = require_missing_tests_detector();
|
|
19941
20430
|
var dead_code_detector_1 = require_dead_code_detector();
|
|
19942
20431
|
var coverage_delta_detector_1 = require_coverage_delta_detector();
|
|
20432
|
+
var secret_scanner_1 = require_secret_scanner();
|
|
20433
|
+
var eval_detector_1 = require_eval_detector();
|
|
20434
|
+
var math_random_detector_1 = require_math_random_detector();
|
|
20435
|
+
var tls_detector_1 = require_tls_detector();
|
|
19943
20436
|
var EXCLUDED_FILE_PATTERNS = [
|
|
19944
20437
|
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
19945
20438
|
/\.test\.(ts|tsx|js|jsx)$/,
|
|
@@ -19968,7 +20461,7 @@ var require_orchestrator = __commonJS({
|
|
|
19968
20461
|
changedFiles: input.changedFiles.filter((f) => !isExcludedFile(f.path))
|
|
19969
20462
|
};
|
|
19970
20463
|
const projectRoot = input.projectRoot ?? deriveProjectRoot(input);
|
|
19971
|
-
const [importGraph, complexityDeltas, runtimeResult, perfViolations, reliabilityViolations, duplicationResult, missingTestsResult, deadCodeResult, coverageDeltaResult] = await Promise.all([
|
|
20464
|
+
const [importGraph, complexityDeltas, runtimeResult, perfViolations, reliabilityViolations, duplicationResult, missingTestsResult, deadCodeResult, coverageDeltaResult, secretViolations, evalViolations, mathRandomViolations, tlsViolations] = await Promise.all([
|
|
19972
20465
|
(0, import_graph_1.buildImportGraph)(filteredInput, policy),
|
|
19973
20466
|
(0, complexity_calculator_1.calculateComplexity)(filteredInput),
|
|
19974
20467
|
(0, runtime_risk_detector_1.detectRuntimeRisks)(filteredInput, policy),
|
|
@@ -19977,7 +20470,11 @@ var require_orchestrator = __commonJS({
|
|
|
19977
20470
|
(0, duplication_detector_1.detectDuplication)(filteredInput),
|
|
19978
20471
|
projectRoot ? (0, missing_tests_detector_1.detectMissingTests)(filteredInput, projectRoot, buildMissingTestsConfig(policy)) : Promise.resolve(null),
|
|
19979
20472
|
(0, dead_code_detector_1.detectDeadCode)(filteredInput, { excludeBarrels: false, excludeTypes: false }, projectRoot),
|
|
19980
|
-
projectRoot ? Promise.resolve((0, coverage_delta_detector_1.detectCoverageDelta)(projectRoot)) : Promise.resolve(null)
|
|
20473
|
+
projectRoot ? Promise.resolve((0, coverage_delta_detector_1.detectCoverageDelta)(projectRoot)) : Promise.resolve(null),
|
|
20474
|
+
(0, secret_scanner_1.detectHardcodedSecrets)(filteredInput, policy),
|
|
20475
|
+
(0, eval_detector_1.detectEvalUsage)(filteredInput, policy),
|
|
20476
|
+
(0, math_random_detector_1.detectMathRandomForSecurity)(filteredInput, policy),
|
|
20477
|
+
(0, tls_detector_1.detectTlsValidationDisabled)(filteredInput, policy)
|
|
19981
20478
|
]);
|
|
19982
20479
|
const runtimeViolations = runtimeResult.violations;
|
|
19983
20480
|
const unflaggedPatterns = runtimeResult.unflaggedPatterns;
|
|
@@ -19992,6 +20489,10 @@ var require_orchestrator = __commonJS({
|
|
|
19992
20489
|
const complexityViolations = complexityDeltasToViolations(complexityDeltas, filteredInput);
|
|
19993
20490
|
const largeFileViolations = detectLargeFiles(filteredInput);
|
|
19994
20491
|
const rawViolations = [
|
|
20492
|
+
...secretViolations,
|
|
20493
|
+
...evalViolations,
|
|
20494
|
+
...mathRandomViolations,
|
|
20495
|
+
...tlsViolations,
|
|
19995
20496
|
...boundaryViolations,
|
|
19996
20497
|
...circularViolations,
|
|
19997
20498
|
...runtimeViolations,
|
|
@@ -20336,7 +20837,7 @@ var require_dist3 = __commonJS({
|
|
|
20336
20837
|
"../../packages/analyzers/dist/index.js"(exports2) {
|
|
20337
20838
|
"use strict";
|
|
20338
20839
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
20339
|
-
exports2.runFullAnalysis = exports2.detectCoverageDelta = exports2.detectDeadCode = exports2.analyzeCrossFileWithAI = exports2.buildReverseImportGraph = exports2.analyzeCrossFile = exports2.detectMissingTests = exports2.detectDuplication = exports2.detectReliabilityIssues = exports2.detectPerformanceRisks = exports2.detectRuntimeRisks = exports2.calculateComplexity = exports2.detectCircularDeps = exports2.checkBoundaries = exports2.buildImportGraph = void 0;
|
|
20840
|
+
exports2.runFullAnalysis = exports2.detectTlsValidationDisabled = exports2.detectMathRandomForSecurity = exports2.detectEvalUsage = exports2.detectHardcodedSecrets = exports2.detectCoverageDelta = exports2.detectDeadCode = exports2.analyzeCrossFileWithAI = exports2.buildReverseImportGraph = exports2.analyzeCrossFile = exports2.detectMissingTests = exports2.detectDuplication = exports2.detectReliabilityIssues = exports2.detectPerformanceRisks = exports2.detectRuntimeRisks = exports2.calculateComplexity = exports2.detectCircularDeps = exports2.checkBoundaries = exports2.buildImportGraph = void 0;
|
|
20340
20841
|
var import_graph_1 = require_import_graph();
|
|
20341
20842
|
Object.defineProperty(exports2, "buildImportGraph", { enumerable: true, get: function() {
|
|
20342
20843
|
return import_graph_1.buildImportGraph;
|
|
@@ -20392,6 +20893,22 @@ var require_dist3 = __commonJS({
|
|
|
20392
20893
|
Object.defineProperty(exports2, "detectCoverageDelta", { enumerable: true, get: function() {
|
|
20393
20894
|
return coverage_delta_detector_1.detectCoverageDelta;
|
|
20394
20895
|
} });
|
|
20896
|
+
var secret_scanner_1 = require_secret_scanner();
|
|
20897
|
+
Object.defineProperty(exports2, "detectHardcodedSecrets", { enumerable: true, get: function() {
|
|
20898
|
+
return secret_scanner_1.detectHardcodedSecrets;
|
|
20899
|
+
} });
|
|
20900
|
+
var eval_detector_1 = require_eval_detector();
|
|
20901
|
+
Object.defineProperty(exports2, "detectEvalUsage", { enumerable: true, get: function() {
|
|
20902
|
+
return eval_detector_1.detectEvalUsage;
|
|
20903
|
+
} });
|
|
20904
|
+
var math_random_detector_1 = require_math_random_detector();
|
|
20905
|
+
Object.defineProperty(exports2, "detectMathRandomForSecurity", { enumerable: true, get: function() {
|
|
20906
|
+
return math_random_detector_1.detectMathRandomForSecurity;
|
|
20907
|
+
} });
|
|
20908
|
+
var tls_detector_1 = require_tls_detector();
|
|
20909
|
+
Object.defineProperty(exports2, "detectTlsValidationDisabled", { enumerable: true, get: function() {
|
|
20910
|
+
return tls_detector_1.detectTlsValidationDisabled;
|
|
20911
|
+
} });
|
|
20395
20912
|
var orchestrator_1 = require_orchestrator();
|
|
20396
20913
|
Object.defineProperty(exports2, "runFullAnalysis", { enumerable: true, get: function() {
|
|
20397
20914
|
return orchestrator_1.runFullAnalysis;
|
|
@@ -20399,6 +20916,81 @@ var require_dist3 = __commonJS({
|
|
|
20399
20916
|
}
|
|
20400
20917
|
});
|
|
20401
20918
|
|
|
20919
|
+
// package.json
|
|
20920
|
+
var require_package = __commonJS({
|
|
20921
|
+
"package.json"(exports2, module2) {
|
|
20922
|
+
module2.exports = {
|
|
20923
|
+
name: "technical-debt-radar",
|
|
20924
|
+
version: "1.16.0",
|
|
20925
|
+
description: "Stop Node.js production crashes before merge. 65 detection rules across 5 categories.",
|
|
20926
|
+
bin: {
|
|
20927
|
+
radar: "dist/index.js",
|
|
20928
|
+
"technical-debt-radar": "dist/index.js"
|
|
20929
|
+
},
|
|
20930
|
+
files: [
|
|
20931
|
+
"dist/",
|
|
20932
|
+
"README.md",
|
|
20933
|
+
"LICENSE"
|
|
20934
|
+
],
|
|
20935
|
+
keywords: [
|
|
20936
|
+
"technical-debt",
|
|
20937
|
+
"code-quality",
|
|
20938
|
+
"architecture",
|
|
20939
|
+
"nestjs",
|
|
20940
|
+
"express",
|
|
20941
|
+
"fastify",
|
|
20942
|
+
"typescript",
|
|
20943
|
+
"linter",
|
|
20944
|
+
"pr-gate",
|
|
20945
|
+
"node",
|
|
20946
|
+
"orm",
|
|
20947
|
+
"static-analysis",
|
|
20948
|
+
"code-review"
|
|
20949
|
+
],
|
|
20950
|
+
author: "Khalid Elattar",
|
|
20951
|
+
license: "MIT",
|
|
20952
|
+
repository: {
|
|
20953
|
+
type: "git",
|
|
20954
|
+
url: "git+https://github.com/khalid-Elattar/technical_dept_radar.git"
|
|
20955
|
+
},
|
|
20956
|
+
homepage: "https://technicaldebtradar.com",
|
|
20957
|
+
engines: {
|
|
20958
|
+
node: ">=18"
|
|
20959
|
+
},
|
|
20960
|
+
scripts: {
|
|
20961
|
+
build: "tsc",
|
|
20962
|
+
"build:publish": "tsup",
|
|
20963
|
+
dev: "ts-node src/index.ts",
|
|
20964
|
+
test: "jest --passWithNoTests",
|
|
20965
|
+
prepublishOnly: "npm run build:publish"
|
|
20966
|
+
},
|
|
20967
|
+
dependencies: {
|
|
20968
|
+
"@anthropic-ai/sdk": "^0.78.0",
|
|
20969
|
+
chalk: "^4.1.2",
|
|
20970
|
+
commander: "^14.0.3",
|
|
20971
|
+
inquirer: "^13.3.0",
|
|
20972
|
+
"js-yaml": "^4.1.1",
|
|
20973
|
+
ora: "^5.4.1",
|
|
20974
|
+
"ts-morph": "^27.0.2",
|
|
20975
|
+
typescript: "^5.9.3"
|
|
20976
|
+
},
|
|
20977
|
+
devDependencies: {
|
|
20978
|
+
"@radar/analyzers": "*",
|
|
20979
|
+
"@radar/policy-engine": "*",
|
|
20980
|
+
"@radar/shared": "*",
|
|
20981
|
+
"@types/inquirer": "^9.0.9",
|
|
20982
|
+
"@types/jest": "^29.5.0",
|
|
20983
|
+
"@types/js-yaml": "^4.0.9",
|
|
20984
|
+
"@types/node": "^22.0.0",
|
|
20985
|
+
jest: "^29.7.0",
|
|
20986
|
+
"ts-jest": "^29.1.0",
|
|
20987
|
+
"ts-node": "^10.9.2",
|
|
20988
|
+
tsup: "^8.5.1"
|
|
20989
|
+
}
|
|
20990
|
+
};
|
|
20991
|
+
}
|
|
20992
|
+
});
|
|
20993
|
+
|
|
20402
20994
|
// src/index.ts
|
|
20403
20995
|
var import_commander = require("commander");
|
|
20404
20996
|
|
|
@@ -20839,6 +21431,45 @@ function printAnonymousScanCTA() {
|
|
|
20839
21431
|
console.log(import_chalk.default.cyan("\u2502") + ` Solo ($15): PR blocking + AI fixes ` + import_chalk.default.cyan("\u2502"));
|
|
20840
21432
|
console.log(import_chalk.default.cyan("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
20841
21433
|
}
|
|
21434
|
+
function getGitRemoteOwnerRepo() {
|
|
21435
|
+
try {
|
|
21436
|
+
const { execSync: execSync3 } = require("child_process");
|
|
21437
|
+
const url = execSync3("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
21438
|
+
const match = url.match(/[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
21439
|
+
if (match) return { owner: match[1], repo: match[2] };
|
|
21440
|
+
} catch {
|
|
21441
|
+
}
|
|
21442
|
+
return null;
|
|
21443
|
+
}
|
|
21444
|
+
function printPostScanHints(isAnonymous, hasViolations) {
|
|
21445
|
+
if (!process.stdout.isTTY) return;
|
|
21446
|
+
const remote = getGitRemoteOwnerRepo();
|
|
21447
|
+
const badgeCmd = remote ? `radar badge --owner ${remote.owner} --repo ${remote.repo}` : "radar badge --owner <owner> --repo <repo>";
|
|
21448
|
+
console.log("");
|
|
21449
|
+
console.log(import_chalk.default.dim(" \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
21450
|
+
if (isAnonymous) {
|
|
21451
|
+
console.log(import_chalk.default.bold(" What's next?"));
|
|
21452
|
+
console.log("");
|
|
21453
|
+
console.log(` ${import_chalk.default.dim("\u2192")} Create a free account for 5 scans/month:`);
|
|
21454
|
+
console.log(` ${import_chalk.default.cyan("radar login")}`);
|
|
21455
|
+
console.log(` ${import_chalk.default.dim("\u2192")} Enforce on every PR with GitHub Actions:`);
|
|
21456
|
+
console.log(` ${import_chalk.default.dim("https://technicaldebtradar.com/docs/integrations/github-action")}`);
|
|
21457
|
+
} else if (hasViolations) {
|
|
21458
|
+
console.log(import_chalk.default.bold(" Fix these violations:"));
|
|
21459
|
+
console.log("");
|
|
21460
|
+
console.log(` ${import_chalk.default.dim("\u2192")} AI-powered fix: ${import_chalk.default.cyan("radar fix")}`);
|
|
21461
|
+
console.log(` ${import_chalk.default.dim("\u2192")} Copy for Claude/Cursor: ${import_chalk.default.cyan("radar scan --format ai-prompt")}`);
|
|
21462
|
+
console.log(` ${import_chalk.default.dim("\u2192")} Add a quality badge: ${import_chalk.default.cyan(badgeCmd)}`);
|
|
21463
|
+
} else {
|
|
21464
|
+
console.log(import_chalk.default.bold(" All clear!"));
|
|
21465
|
+
console.log("");
|
|
21466
|
+
console.log(` ${import_chalk.default.dim("\u2192")} Add a quality badge to your README:`);
|
|
21467
|
+
console.log(` ${import_chalk.default.cyan(badgeCmd)}`);
|
|
21468
|
+
console.log(` ${import_chalk.default.dim("\u2192")} Enforce on every PR: ${import_chalk.default.dim("https://technicaldebtradar.com/docs/integrations/github-action")}`);
|
|
21469
|
+
}
|
|
21470
|
+
console.log(import_chalk.default.dim(" \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
21471
|
+
console.log("");
|
|
21472
|
+
}
|
|
20842
21473
|
async function enforceAuth(allowAnonymous = false) {
|
|
20843
21474
|
const client = RadarApiClient.fromConfigOrEnv();
|
|
20844
21475
|
if (!client) {
|
|
@@ -20982,6 +21613,7 @@ async function scanCommand(targetPath, options) {
|
|
|
20982
21613
|
if (isAnonymous) {
|
|
20983
21614
|
markAnonymousScanUsed();
|
|
20984
21615
|
printAnonymousScanCTA();
|
|
21616
|
+
printPostScanHints(true, result.violations.length > 0);
|
|
20985
21617
|
}
|
|
20986
21618
|
if (client) {
|
|
20987
21619
|
const blocking = result.violations.filter((v) => v.gateAction === "block");
|
|
@@ -21047,6 +21679,9 @@ async function scanCommand(targetPath, options) {
|
|
|
21047
21679
|
console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not sync results to dashboard${detail}`));
|
|
21048
21680
|
}
|
|
21049
21681
|
}
|
|
21682
|
+
if (!isAnonymous) {
|
|
21683
|
+
printPostScanHints(false, result.violations.length > 0);
|
|
21684
|
+
}
|
|
21050
21685
|
if (mode === "warn") {
|
|
21051
21686
|
if (gateEval.gateWouldFail) {
|
|
21052
21687
|
console.log(import_chalk.default.yellow("\n \u26A0\uFE0F Warning mode \u2014 gate would have BLOCKED this PR in enforce mode"));
|
|
@@ -22025,10 +22660,23 @@ async function quickScanFile(targetPath, filePath, options) {
|
|
|
22025
22660
|
}
|
|
22026
22661
|
}
|
|
22027
22662
|
function resolveViolationPath(vFile, projectRoot) {
|
|
22028
|
-
|
|
22029
|
-
|
|
22030
|
-
|
|
22031
|
-
|
|
22663
|
+
let resolved;
|
|
22664
|
+
if (path5.isAbsolute(vFile)) {
|
|
22665
|
+
resolved = vFile;
|
|
22666
|
+
} else {
|
|
22667
|
+
const withSlash = "/" + vFile;
|
|
22668
|
+
if (withSlash.startsWith(projectRoot)) {
|
|
22669
|
+
resolved = withSlash;
|
|
22670
|
+
} else {
|
|
22671
|
+
resolved = path5.resolve(projectRoot, vFile);
|
|
22672
|
+
}
|
|
22673
|
+
}
|
|
22674
|
+
const normalized = path5.normalize(resolved);
|
|
22675
|
+
const normalizedRoot = path5.normalize(projectRoot) + path5.sep;
|
|
22676
|
+
if (!normalized.startsWith(normalizedRoot) && normalized !== path5.normalize(projectRoot)) {
|
|
22677
|
+
throw new Error(`Path traversal blocked: ${vFile} resolves outside project root`);
|
|
22678
|
+
}
|
|
22679
|
+
return normalized;
|
|
22032
22680
|
}
|
|
22033
22681
|
function detectTestCommand(projectRoot) {
|
|
22034
22682
|
try {
|
|
@@ -22496,6 +23144,7 @@ ${relativePath} (${fileViolations.length} violations)
|
|
|
22496
23144
|
${newCount} violations remaining. Run radar fix again or fix manually.`));
|
|
22497
23145
|
} else {
|
|
22498
23146
|
console.log(import_chalk4.default.green("\n All violations fixed! Code is clean."));
|
|
23147
|
+
console.log(import_chalk4.default.dim(` \u2192 Add a quality badge: ${import_chalk4.default.cyan("radar badge --owner <owner> --repo <repo>")}`));
|
|
22499
23148
|
}
|
|
22500
23149
|
}
|
|
22501
23150
|
}
|
|
@@ -22732,9 +23381,10 @@ async function loginCommand(options) {
|
|
|
22732
23381
|
console.log(`If the browser doesn't open, visit: ${import_chalk6.default.underline(authUrl)}
|
|
22733
23382
|
`);
|
|
22734
23383
|
try {
|
|
22735
|
-
const {
|
|
23384
|
+
const { execFile } = await import("child_process");
|
|
22736
23385
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
22737
|
-
|
|
23386
|
+
execFile(cmd, [authUrl], () => {
|
|
23387
|
+
});
|
|
22738
23388
|
} catch {
|
|
22739
23389
|
}
|
|
22740
23390
|
const readline = await import("readline");
|
|
@@ -22847,16 +23497,17 @@ async function upgradeCommand() {
|
|
|
22847
23497
|
console.log(`If the browser doesn't open, visit: ${import_chalk9.default.underline(url)}
|
|
22848
23498
|
`);
|
|
22849
23499
|
try {
|
|
22850
|
-
const {
|
|
23500
|
+
const { execFile } = await import("child_process");
|
|
22851
23501
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
22852
|
-
|
|
23502
|
+
execFile(cmd, [url], () => {
|
|
23503
|
+
});
|
|
22853
23504
|
} catch {
|
|
22854
23505
|
}
|
|
22855
23506
|
}
|
|
22856
23507
|
|
|
22857
23508
|
// src/index.ts
|
|
22858
23509
|
var program = new import_commander.Command();
|
|
22859
|
-
program.name("radar").description("Technical Debt Radar \u2014 Architecture & Runtime Safety CLI").version(
|
|
23510
|
+
program.name("radar").description("Technical Debt Radar \u2014 Architecture & Runtime Safety CLI").version(require_package().version);
|
|
22860
23511
|
program.command("scan <path>").description("Scan a directory for technical debt").option("-c, --config <path>", "Path to radar.yml", "./radar.yml").option("-r, --rules <path>", "Path to rules.yml (default: ./rules.yml)").option("-f, --format <type>", "Output format: text, json, table, markdown, ai-prompt, ai-json, pr", "text").option("--fail-on <severity>", "Exit code 1 on: critical, warning", "critical").option("--no-ai", "Skip AI analysis (faster, no API cost)").action(async (targetPath, options) => {
|
|
22861
23512
|
await scanCommand(targetPath, options);
|
|
22862
23513
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "technical-debt-radar",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Stop Node.js production crashes before merge.
|
|
3
|
+
"version": "1.16.0",
|
|
4
|
+
"description": "Stop Node.js production crashes before merge. 65 detection rules across 5 categories.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"radar": "dist/index.js",
|
|
7
7
|
"technical-debt-radar": "dist/index.js"
|