technical-debt-radar 1.15.0 → 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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +677 -26
  3. 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. 47 detection patterns across 5 categories. Works with NestJS, Express, Fastify, Koa, Hapi + 7 ORMs.
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 (11 patterns)
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 = "", normalize) {
3274
- if (normalize !== false)
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 normalize(uri, options) {
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) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(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 hasLimit = /\bLIMIT\b/i.test(sqlText);
16588
- if (hasLimit && !isUnsafe)
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 (!hasLimit) {
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
- if (path5.isAbsolute(vFile)) return vFile;
22029
- const withSlash = "/" + vFile;
22030
- if (withSlash.startsWith(projectRoot)) return withSlash;
22031
- return path5.resolve(projectRoot, vFile);
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 { exec } = await import("child_process");
23384
+ const { execFile } = await import("child_process");
22736
23385
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
22737
- exec(`${cmd} ${authUrl}`);
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 { exec } = await import("child_process");
23500
+ const { execFile } = await import("child_process");
22851
23501
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
22852
- exec(`${cmd} ${url}`);
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("1.0.1");
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
  });
@@ -22878,7 +23529,7 @@ program.command("validate").description("Validate radar.yml + rules.yml syntax a
22878
23529
  program.command("run <path>").description("CI-friendly scan \u2014 exits 1 on critical violations").option("-c, --config <path>", "Path to radar.yml", "./radar.yml").option("-r, --rules <path>", "Path to rules.yml (default: ./rules.yml)").action(async (targetPath, options) => {
22879
23530
  await runCommand(targetPath, options);
22880
23531
  });
22881
- program.command("badge").description("Generate badge markdown for your README").requiredOption("--owner <owner>", "Repository owner (org or user)").requiredOption("--repo <repo>", "Repository name").option("--api-url <url>", "Radar API base URL", "https://radar-api.example.com").action((options) => {
23532
+ program.command("badge").description("Generate badge markdown for your README").requiredOption("--owner <owner>", "Repository owner (org or user)").requiredOption("--repo <repo>", "Repository name").option("--api-url <url>", "Radar API base URL", loadConfig()?.apiUrl || getDefaultApiUrl()).action((options) => {
22882
23533
  badgeCommand(options);
22883
23534
  });
22884
23535
  var packCmd = program.command("pack").description("Manage rule packs \u2014 pre-configured rules for specific stacks");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.15.0",
4
- "description": "Stop Node.js production crashes before merge. 47 detection patterns across 5 categories.",
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"