technical-debt-radar 1.16.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +992 -9
  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, security issues, and architecture drift in your PRs. 65 deterministic detection rules across 6 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. 74 deterministic detection rules across 6 categories. Works with NestJS, Express, Fastify, Koa, Hapi + 7 ORMs.
6
6
 
7
7
  ## Quick Start
8
8
 
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.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;
42
+ exports2.DEBT_DELTA_WARN_THRESHOLD = exports2.DEBT_DELTA_BLOCK_THRESHOLD = exports2.DEFAULT_NESTING_DEPTH_THRESHOLD = exports2.DEFAULT_PARAMETER_COUNT_THRESHOLD = exports2.DEFAULT_FUNCTION_LENGTH_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,
@@ -119,7 +119,13 @@ var require_constants = __commonJS({
119
119
  DEBUG_CONSOLE_LOG: "debug-console-log-in-production",
120
120
  LOW_TEST_COVERAGE: "low-test-coverage",
121
121
  LOW_BRANCH_COVERAGE: "low-branch-coverage",
122
- COVERAGE_DROP: "coverage-drop"
122
+ COVERAGE_DROP: "coverage-drop",
123
+ PROCESS_ENV_TYPO: "process-env-typo",
124
+ FUNCTION_TOO_LONG: "function-too-long",
125
+ TOO_MANY_PARAMETERS: "too-many-parameters",
126
+ DEEP_NESTING: "deep-nesting",
127
+ CONSOLE_LOG_IN_PRODUCTION: "console-log-in-production",
128
+ STRAY_TODO_COMMENT: "stray-todo-comment"
123
129
  };
124
130
  exports2.ARCHITECTURE_RULES = {
125
131
  LAYER_VIOLATION: "layer-boundary-violation",
@@ -142,7 +148,10 @@ var require_constants = __commonJS({
142
148
  HARDCODED_SECRET_LITERAL: "hardcoded-secret-literal",
143
149
  EVAL_USAGE: "eval-usage",
144
150
  MATH_RANDOM_FOR_SECURITY: "math-random-for-security",
145
- TLS_VALIDATION_DISABLED: "tls-validation-disabled"
151
+ TLS_VALIDATION_DISABLED: "tls-validation-disabled",
152
+ CORS_WILDCARD_CREDENTIALS: "cors-wildcard-credentials",
153
+ JWT_INSECURE_OPTIONS: "jwt-insecure-options",
154
+ DANGEROUSLY_SET_INNER_HTML_UNSANITIZED: "dangerously-set-inner-html-unsanitized"
146
155
  };
147
156
  exports2.MAX_CROSS_FILE_SUSPECTS = 10;
148
157
  exports2.MAX_CALLER_TRACE_DEPTH = 2;
@@ -154,6 +163,9 @@ var require_constants = __commonJS({
154
163
  exports2.MAX_CHANGED_FILES_PER_PR = 100;
155
164
  exports2.DEFAULT_COMPLEXITY_THRESHOLD = 10;
156
165
  exports2.DEFAULT_LARGE_FILE_THRESHOLD = 300;
166
+ exports2.DEFAULT_FUNCTION_LENGTH_THRESHOLD = 50;
167
+ exports2.DEFAULT_PARAMETER_COUNT_THRESHOLD = 4;
168
+ exports2.DEFAULT_NESTING_DEPTH_THRESHOLD = 4;
157
169
  exports2.DEBT_DELTA_BLOCK_THRESHOLD = 15;
158
170
  exports2.DEBT_DELTA_WARN_THRESHOLD = 8;
159
171
  }
@@ -1058,6 +1070,12 @@ var require_parser = __commonJS({
1058
1070
  }
1059
1071
  result.ignore_tests_for = m.ignore_tests_for.map(String);
1060
1072
  }
1073
+ if (m.check_env_drift !== void 0) {
1074
+ if (typeof m.check_env_drift !== "boolean") {
1075
+ throw new PolicyValidationError("maintainability.check_env_drift must be a boolean");
1076
+ }
1077
+ result.check_env_drift = m.check_env_drift;
1078
+ }
1061
1079
  return result;
1062
1080
  }
1063
1081
  function parseScoring(value) {
@@ -7714,7 +7732,8 @@ var require_validator = __commonJS({
7714
7732
  properties: {
7715
7733
  require_tests: { type: "boolean" },
7716
7734
  require_tests_for: { type: "array", items: { type: "string" } },
7717
- ignore_tests_for: { type: "array", items: { type: "string" } }
7735
+ ignore_tests_for: { type: "array", items: { type: "string" } },
7736
+ check_env_drift: { type: "boolean" }
7718
7737
  },
7719
7738
  additionalProperties: false
7720
7739
  }
@@ -20372,6 +20391,919 @@ var require_tls_detector = __commonJS({
20372
20391
  }
20373
20392
  });
20374
20393
 
20394
+ // ../../packages/analyzers/dist/typescript/parsed-files.js
20395
+ var require_parsed_files = __commonJS({
20396
+ "../../packages/analyzers/dist/typescript/parsed-files.js"(exports2) {
20397
+ "use strict";
20398
+ Object.defineProperty(exports2, "__esModule", { value: true });
20399
+ exports2.parseChangedFiles = parseChangedFiles;
20400
+ var ts_morph_1 = require("ts-morph");
20401
+ function parseChangedFiles(input) {
20402
+ const project = new ts_morph_1.Project({ useInMemoryFileSystem: true });
20403
+ const parsed = [];
20404
+ for (const file of input.changedFiles) {
20405
+ if (file.status === "deleted")
20406
+ continue;
20407
+ if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
20408
+ continue;
20409
+ parsed.push({
20410
+ path: file.path,
20411
+ sourceFile: project.createSourceFile(file.path, file.content)
20412
+ });
20413
+ }
20414
+ return parsed;
20415
+ }
20416
+ }
20417
+ });
20418
+
20419
+ // ../../packages/analyzers/dist/typescript/cors-detector.js
20420
+ var require_cors_detector = __commonJS({
20421
+ "../../packages/analyzers/dist/typescript/cors-detector.js"(exports2) {
20422
+ "use strict";
20423
+ Object.defineProperty(exports2, "__esModule", { value: true });
20424
+ exports2.detectCorsWildcardCredentials = detectCorsWildcardCredentials;
20425
+ var shared_1 = require_dist();
20426
+ var ts_morph_1 = require("ts-morph");
20427
+ var parsed_files_1 = require_parsed_files();
20428
+ var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.fixture\.|\.example\.|\.stories\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|test|tests|e2e|mocks)(\/|$)/i;
20429
+ async function detectCorsWildcardCredentials(input, policy, parsed) {
20430
+ const violations = [];
20431
+ for (const { path: path9, sourceFile } of parsed ?? (0, parsed_files_1.parseChangedFiles)(input)) {
20432
+ if (EXCLUDED_PATH_REGEX.test(path9))
20433
+ continue;
20434
+ scanFile(sourceFile, path9, policy, violations);
20435
+ }
20436
+ return violations;
20437
+ }
20438
+ function scanFile(sourceFile, filePath, policy, violations) {
20439
+ sourceFile.forEachDescendant((node) => {
20440
+ if (!ts_morph_1.Node.isCallExpression(node))
20441
+ return;
20442
+ const options = corsOptionsObject(node);
20443
+ if (!options)
20444
+ return;
20445
+ if (!hasDangerousCorsCombo(options))
20446
+ return;
20447
+ violations.push({
20448
+ category: "runtime_risk",
20449
+ type: shared_1.SECURITY_RULES.CORS_WILDCARD_CREDENTIALS,
20450
+ ruleId: shared_1.SECURITY_RULES.CORS_WILDCARD_CREDENTIALS,
20451
+ severity: "critical",
20452
+ source: "deterministic",
20453
+ confidence: "high",
20454
+ file: filePath,
20455
+ line: node.getStartLineNumber(),
20456
+ function: getEnclosingFunctionName(node),
20457
+ message: "CORS is configured with a wildcard/reflected origin and credentials: true \u2014 any website can make authenticated cross-origin requests to this API.",
20458
+ suggestion: "When credentials are enabled, set `origin` to an explicit allow-list of trusted domains. A wildcard (or reflected) origin cannot be combined with credentials.",
20459
+ debtPoints: policy.scoring.runtime_risk_critical,
20460
+ gateAction: "block"
20461
+ });
20462
+ });
20463
+ }
20464
+ function corsOptionsObject(call) {
20465
+ const expr = call.getExpression();
20466
+ const args = call.getArguments();
20467
+ if (ts_morph_1.Node.isIdentifier(expr) && expr.getText() === "cors") {
20468
+ return objectLiteralArg(args, 0);
20469
+ }
20470
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
20471
+ const method = expr.getName();
20472
+ if (method === "enableCors") {
20473
+ return objectLiteralArg(args, 0);
20474
+ }
20475
+ if (method === "register" && argsReferenceCors(args)) {
20476
+ for (const arg of args) {
20477
+ if (ts_morph_1.Node.isObjectLiteralExpression(arg))
20478
+ return arg;
20479
+ }
20480
+ }
20481
+ }
20482
+ return void 0;
20483
+ }
20484
+ function objectLiteralArg(args, index) {
20485
+ const arg = args[index];
20486
+ return arg && ts_morph_1.Node.isObjectLiteralExpression(arg) ? arg : void 0;
20487
+ }
20488
+ function argsReferenceCors(args) {
20489
+ return args.some((a) => /cors/i.test(a.getText()));
20490
+ }
20491
+ function hasDangerousCorsCombo(obj) {
20492
+ let originIsWildcard = false;
20493
+ let credentialsEnabled = false;
20494
+ for (const prop of obj.getProperties()) {
20495
+ if (!ts_morph_1.Node.isPropertyAssignment(prop))
20496
+ continue;
20497
+ const name = prop.getName().replace(/['"`]/g, "");
20498
+ const init = prop.getInitializer();
20499
+ if (!init)
20500
+ continue;
20501
+ if (name === "origin" && isWildcardOrigin(init)) {
20502
+ originIsWildcard = true;
20503
+ } else if (name === "credentials" && init.getText().trim() === "true") {
20504
+ credentialsEnabled = true;
20505
+ }
20506
+ }
20507
+ return originIsWildcard && credentialsEnabled;
20508
+ }
20509
+ function isWildcardOrigin(init) {
20510
+ if (ts_morph_1.Node.isStringLiteral(init))
20511
+ return init.getLiteralValue() === "*";
20512
+ if (init.getText().trim() === "true")
20513
+ return true;
20514
+ if (ts_morph_1.Node.isArrayLiteralExpression(init)) {
20515
+ return init.getElements().some((el) => ts_morph_1.Node.isStringLiteral(el) && el.getLiteralValue() === "*");
20516
+ }
20517
+ return false;
20518
+ }
20519
+ function getEnclosingFunctionName(node) {
20520
+ const fn = node.getFirstAncestor((a) => ts_morph_1.Node.isFunctionDeclaration(a) || ts_morph_1.Node.isMethodDeclaration(a));
20521
+ if (fn && (ts_morph_1.Node.isFunctionDeclaration(fn) || ts_morph_1.Node.isMethodDeclaration(fn))) {
20522
+ return fn.getName() ?? void 0;
20523
+ }
20524
+ return void 0;
20525
+ }
20526
+ }
20527
+ });
20528
+
20529
+ // ../../packages/analyzers/dist/typescript/jwt-detector.js
20530
+ var require_jwt_detector = __commonJS({
20531
+ "../../packages/analyzers/dist/typescript/jwt-detector.js"(exports2) {
20532
+ "use strict";
20533
+ Object.defineProperty(exports2, "__esModule", { value: true });
20534
+ exports2.detectJwtInsecureOptions = detectJwtInsecureOptions;
20535
+ var shared_1 = require_dist();
20536
+ var ts_morph_1 = require("ts-morph");
20537
+ var parsed_files_1 = require_parsed_files();
20538
+ var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.fixture\.|\.example\.|\.stories\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|test|tests|e2e|mocks)(\/|$)/i;
20539
+ var MIN_SECRET_LENGTH = 32;
20540
+ var SIGN_METHODS = /* @__PURE__ */ new Set(["sign", "signAsync"]);
20541
+ var VERIFY_METHODS = /* @__PURE__ */ new Set(["verify", "verifyAsync"]);
20542
+ async function detectJwtInsecureOptions(input, policy, parsed) {
20543
+ const violations = [];
20544
+ for (const { path: path9, sourceFile } of parsed ?? (0, parsed_files_1.parseChangedFiles)(input)) {
20545
+ if (EXCLUDED_PATH_REGEX.test(path9))
20546
+ continue;
20547
+ scanFile(sourceFile, path9, policy, violations);
20548
+ }
20549
+ return violations;
20550
+ }
20551
+ function scanFile(sourceFile, filePath, policy, violations) {
20552
+ sourceFile.forEachDescendant((node) => {
20553
+ if (!ts_morph_1.Node.isCallExpression(node))
20554
+ return;
20555
+ const call = jwtCallKind(node);
20556
+ if (!call)
20557
+ return;
20558
+ const { kind, style } = call;
20559
+ const args = node.getArguments();
20560
+ const line = node.getStartLineNumber();
20561
+ const fn = getEnclosingFunctionName(node);
20562
+ const secretArg = style === "lib" ? args[1] : void 0;
20563
+ const optionsArg = style === "lib" ? args[2] : args[1];
20564
+ const optionsObj = optionsArg && ts_morph_1.Node.isObjectLiteralExpression(optionsArg) ? optionsArg : void 0;
20565
+ if (optionsObj && hasAlgorithmNone(optionsObj)) {
20566
+ violations.push({
20567
+ category: "runtime_risk",
20568
+ type: shared_1.SECURITY_RULES.JWT_INSECURE_OPTIONS,
20569
+ ruleId: shared_1.SECURITY_RULES.JWT_INSECURE_OPTIONS,
20570
+ severity: "critical",
20571
+ source: "deterministic",
20572
+ confidence: "high",
20573
+ file: filePath,
20574
+ line,
20575
+ function: fn,
20576
+ message: "JWT is configured with the 'none' algorithm \u2014 the token signature is disabled, so any forged token is accepted.",
20577
+ suggestion: "Pin an explicit signing algorithm (e.g. algorithm: 'HS256' or 'RS256') and never include 'none' in the algorithms allow-list.",
20578
+ debtPoints: policy.scoring.runtime_risk_critical,
20579
+ gateAction: "block"
20580
+ });
20581
+ }
20582
+ if (kind === "sign" && !hasExpiresIn(optionsArg, optionsObj)) {
20583
+ violations.push({
20584
+ category: "runtime_risk",
20585
+ type: shared_1.SECURITY_RULES.JWT_INSECURE_OPTIONS,
20586
+ ruleId: shared_1.SECURITY_RULES.JWT_INSECURE_OPTIONS,
20587
+ severity: "warning",
20588
+ source: "deterministic",
20589
+ confidence: "high",
20590
+ file: filePath,
20591
+ line,
20592
+ function: fn,
20593
+ message: "JWT is signed without an `expiresIn` option \u2014 the issued token never expires and cannot be aged out.",
20594
+ suggestion: "Pass an explicit lifetime, e.g. sign(payload, secret, { expiresIn: '15m' }).",
20595
+ debtPoints: policy.scoring.runtime_risk_warning,
20596
+ gateAction: "warn"
20597
+ });
20598
+ }
20599
+ const secretLen = literalSecretLength(secretArg);
20600
+ if (secretLen !== void 0 && secretLen < MIN_SECRET_LENGTH) {
20601
+ violations.push({
20602
+ category: "runtime_risk",
20603
+ type: shared_1.SECURITY_RULES.JWT_INSECURE_OPTIONS,
20604
+ ruleId: shared_1.SECURITY_RULES.JWT_INSECURE_OPTIONS,
20605
+ severity: "warning",
20606
+ source: "deterministic",
20607
+ confidence: "high",
20608
+ file: filePath,
20609
+ line,
20610
+ function: fn,
20611
+ message: `JWT secret is a ${secretLen}-character string literal \u2014 HMAC secrets shorter than ${MIN_SECRET_LENGTH} characters are brute-forceable, and a literal secret is also hardcoded.`,
20612
+ suggestion: 'Load the secret from an environment variable and use at least 32 random characters (e.g. `crypto.randomBytes(32).toString("hex")`).',
20613
+ debtPoints: policy.scoring.runtime_risk_warning,
20614
+ gateAction: "warn"
20615
+ });
20616
+ }
20617
+ });
20618
+ }
20619
+ function jwtCallKind(call) {
20620
+ const expr = call.getExpression();
20621
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
20622
+ return void 0;
20623
+ const method = expr.getName();
20624
+ const isSign = SIGN_METHODS.has(method);
20625
+ const isVerify = VERIFY_METHODS.has(method);
20626
+ if (!isSign && !isVerify)
20627
+ return void 0;
20628
+ const receiver = expr.getExpression().getText();
20629
+ if (!/jwt|jsonwebtoken/i.test(receiver))
20630
+ return void 0;
20631
+ return {
20632
+ kind: isSign ? "sign" : "verify",
20633
+ style: /jwtService/i.test(receiver) ? "service" : "lib"
20634
+ };
20635
+ }
20636
+ function hasAlgorithmNone(obj) {
20637
+ for (const prop of obj.getProperties()) {
20638
+ if (!ts_morph_1.Node.isPropertyAssignment(prop))
20639
+ continue;
20640
+ const name = prop.getName().replace(/['"`]/g, "");
20641
+ if (name !== "algorithm" && name !== "algorithms")
20642
+ continue;
20643
+ const init = prop.getInitializer();
20644
+ if (!init)
20645
+ continue;
20646
+ if (isNoneLiteral(init))
20647
+ return true;
20648
+ if (ts_morph_1.Node.isArrayLiteralExpression(init)) {
20649
+ if (init.getElements().some(isNoneLiteral))
20650
+ return true;
20651
+ }
20652
+ }
20653
+ return false;
20654
+ }
20655
+ function isNoneLiteral(node) {
20656
+ if (ts_morph_1.Node.isStringLiteral(node) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node)) {
20657
+ return node.getLiteralValue().toLowerCase() === "none";
20658
+ }
20659
+ return false;
20660
+ }
20661
+ function hasExpiresIn(optionsArg, optionsObj) {
20662
+ if (!optionsArg)
20663
+ return false;
20664
+ if (!optionsObj)
20665
+ return true;
20666
+ return optionsObj.getProperties().some((prop) => {
20667
+ if (ts_morph_1.Node.isPropertyAssignment(prop) || ts_morph_1.Node.isShorthandPropertyAssignment(prop)) {
20668
+ return prop.getName().replace(/['"`]/g, "") === "expiresIn";
20669
+ }
20670
+ return false;
20671
+ });
20672
+ }
20673
+ function literalSecretLength(node) {
20674
+ if (!node)
20675
+ return void 0;
20676
+ if (ts_morph_1.Node.isStringLiteral(node) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node)) {
20677
+ return node.getLiteralValue().length;
20678
+ }
20679
+ return void 0;
20680
+ }
20681
+ function getEnclosingFunctionName(node) {
20682
+ const fn = node.getFirstAncestor((a) => ts_morph_1.Node.isFunctionDeclaration(a) || ts_morph_1.Node.isMethodDeclaration(a));
20683
+ if (fn && (ts_morph_1.Node.isFunctionDeclaration(fn) || ts_morph_1.Node.isMethodDeclaration(fn))) {
20684
+ return fn.getName() ?? void 0;
20685
+ }
20686
+ return void 0;
20687
+ }
20688
+ }
20689
+ });
20690
+
20691
+ // ../../packages/analyzers/dist/typescript/jsx-xss-detector.js
20692
+ var require_jsx_xss_detector = __commonJS({
20693
+ "../../packages/analyzers/dist/typescript/jsx-xss-detector.js"(exports2) {
20694
+ "use strict";
20695
+ Object.defineProperty(exports2, "__esModule", { value: true });
20696
+ exports2.detectDangerouslySetInnerHtml = detectDangerouslySetInnerHtml;
20697
+ var shared_1 = require_dist();
20698
+ var ts_morph_1 = require("ts-morph");
20699
+ var parsed_files_1 = require_parsed_files();
20700
+ var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.fixture\.|\.stories\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|test|tests|e2e|mocks)(\/|$)/i;
20701
+ var SANITIZER_NAME_REGEX = /^(sanitize|sanitizeHtml|xss|escape|escapeHtml|striptags|purify)$/i;
20702
+ async function detectDangerouslySetInnerHtml(input, policy, parsed) {
20703
+ const violations = [];
20704
+ for (const { path: path9, sourceFile } of parsed ?? (0, parsed_files_1.parseChangedFiles)(input)) {
20705
+ if (!/\.(tsx|jsx)$/.test(path9))
20706
+ continue;
20707
+ if (EXCLUDED_PATH_REGEX.test(path9))
20708
+ continue;
20709
+ scanFile(sourceFile, path9, policy, violations);
20710
+ }
20711
+ return violations;
20712
+ }
20713
+ function scanFile(sourceFile, filePath, policy, violations) {
20714
+ sourceFile.forEachDescendant((node) => {
20715
+ if (!ts_morph_1.Node.isJsxAttribute(node))
20716
+ return;
20717
+ if (node.getNameNode().getText() !== "dangerouslySetInnerHTML")
20718
+ return;
20719
+ const htmlValue = extractHtmlValue(node);
20720
+ if (!htmlValue)
20721
+ return;
20722
+ if (isSafeHtmlValue(htmlValue))
20723
+ return;
20724
+ violations.push({
20725
+ category: "runtime_risk",
20726
+ type: shared_1.SECURITY_RULES.DANGEROUSLY_SET_INNER_HTML_UNSANITIZED,
20727
+ ruleId: shared_1.SECURITY_RULES.DANGEROUSLY_SET_INNER_HTML_UNSANITIZED,
20728
+ severity: "warning",
20729
+ source: "deterministic",
20730
+ confidence: "medium",
20731
+ file: filePath,
20732
+ line: node.getStartLineNumber(),
20733
+ message: "dangerouslySetInnerHTML is set from a value that is not a static string or a sanitizer call \u2014 untrusted HTML here is a cross-site scripting (XSS) sink.",
20734
+ suggestion: "Sanitize the HTML before insertion, e.g. dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(value) }}, or render the value as text instead of HTML.",
20735
+ debtPoints: policy.scoring.runtime_risk_warning,
20736
+ gateAction: "warn"
20737
+ });
20738
+ });
20739
+ }
20740
+ function extractHtmlValue(attr) {
20741
+ if (!ts_morph_1.Node.isJsxAttribute(attr))
20742
+ return void 0;
20743
+ const init = attr.getInitializer();
20744
+ if (!init || !ts_morph_1.Node.isJsxExpression(init))
20745
+ return void 0;
20746
+ const expr = init.getExpression();
20747
+ if (!expr || !ts_morph_1.Node.isObjectLiteralExpression(expr))
20748
+ return void 0;
20749
+ for (const prop of expr.getProperties()) {
20750
+ if (ts_morph_1.Node.isPropertyAssignment(prop)) {
20751
+ if (prop.getName().replace(/['"`]/g, "") === "__html") {
20752
+ return prop.getInitializer();
20753
+ }
20754
+ } else if (ts_morph_1.Node.isShorthandPropertyAssignment(prop)) {
20755
+ if (prop.getName() === "__html") {
20756
+ return prop.getNameNode();
20757
+ }
20758
+ }
20759
+ }
20760
+ return void 0;
20761
+ }
20762
+ function isSafeHtmlValue(value) {
20763
+ if (ts_morph_1.Node.isStringLiteral(value) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(value)) {
20764
+ return true;
20765
+ }
20766
+ if (ts_morph_1.Node.isCallExpression(value)) {
20767
+ return isSanitizerCall(value);
20768
+ }
20769
+ return false;
20770
+ }
20771
+ function isSanitizerCall(call) {
20772
+ if (!ts_morph_1.Node.isCallExpression(call))
20773
+ return false;
20774
+ const expr = call.getExpression();
20775
+ let name;
20776
+ if (ts_morph_1.Node.isIdentifier(expr)) {
20777
+ name = expr.getText();
20778
+ } else if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
20779
+ name = expr.getName();
20780
+ }
20781
+ return name !== void 0 && SANITIZER_NAME_REGEX.test(name);
20782
+ }
20783
+ }
20784
+ });
20785
+
20786
+ // ../../packages/analyzers/dist/typescript/env-typo-detector.js
20787
+ var require_env_typo_detector = __commonJS({
20788
+ "../../packages/analyzers/dist/typescript/env-typo-detector.js"(exports2) {
20789
+ "use strict";
20790
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
20791
+ if (k2 === void 0) k2 = k;
20792
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20793
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20794
+ desc = { enumerable: true, get: function() {
20795
+ return m[k];
20796
+ } };
20797
+ }
20798
+ Object.defineProperty(o, k2, desc);
20799
+ }) : (function(o, m, k, k2) {
20800
+ if (k2 === void 0) k2 = k;
20801
+ o[k2] = m[k];
20802
+ }));
20803
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
20804
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20805
+ }) : function(o, v) {
20806
+ o["default"] = v;
20807
+ });
20808
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
20809
+ var ownKeys = function(o) {
20810
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
20811
+ var ar = [];
20812
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
20813
+ return ar;
20814
+ };
20815
+ return ownKeys(o);
20816
+ };
20817
+ return function(mod) {
20818
+ if (mod && mod.__esModule) return mod;
20819
+ var result = {};
20820
+ if (mod != null) {
20821
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
20822
+ }
20823
+ __setModuleDefault(result, mod);
20824
+ return result;
20825
+ };
20826
+ })();
20827
+ Object.defineProperty(exports2, "__esModule", { value: true });
20828
+ exports2.detectProcessEnvTypo = detectProcessEnvTypo;
20829
+ var fs9 = __importStar(require("fs"));
20830
+ var path9 = __importStar(require("path"));
20831
+ var shared_1 = require_dist();
20832
+ var ts_morph_1 = require("ts-morph");
20833
+ var parsed_files_1 = require_parsed_files();
20834
+ var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.fixture\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|test|tests|e2e|mocks)(\/|$)/i;
20835
+ var PLATFORM_PROVIDED_ENV_VARS = /* @__PURE__ */ new Set([
20836
+ // Node / shell runtime
20837
+ "NODE_ENV",
20838
+ "PORT",
20839
+ "PWD",
20840
+ "HOME",
20841
+ "USER",
20842
+ "PATH",
20843
+ "SHELL",
20844
+ "TERM",
20845
+ "TZ",
20846
+ "LANG",
20847
+ "LC_ALL",
20848
+ "LC_TIME",
20849
+ "DEBUG",
20850
+ "NODE_OPTIONS",
20851
+ "CI",
20852
+ // GitHub Actions — runner-injected, NOT app config
20853
+ "GITHUB_ACTIONS",
20854
+ "GITHUB_TOKEN",
20855
+ "GITHUB_SHA",
20856
+ "GITHUB_REPOSITORY",
20857
+ "GITHUB_REPOSITORY_OWNER",
20858
+ "GITHUB_REF",
20859
+ "GITHUB_REF_NAME",
20860
+ "GITHUB_REF_TYPE",
20861
+ "GITHUB_HEAD_REF",
20862
+ "GITHUB_BASE_REF",
20863
+ "GITHUB_RUN_ID",
20864
+ "GITHUB_RUN_NUMBER",
20865
+ "GITHUB_RUN_ATTEMPT",
20866
+ "GITHUB_WORKFLOW",
20867
+ "GITHUB_EVENT_NAME",
20868
+ "GITHUB_EVENT_PATH",
20869
+ "GITHUB_WORKSPACE",
20870
+ "GITHUB_ACTOR",
20871
+ "GITHUB_ACTOR_ID",
20872
+ "GITHUB_API_URL",
20873
+ "GITHUB_SERVER_URL",
20874
+ "GITHUB_GRAPHQL_URL",
20875
+ "GITHUB_JOB",
20876
+ "GITHUB_ACTION",
20877
+ "GITHUB_ACTION_PATH",
20878
+ "GITHUB_ACTION_REPOSITORY",
20879
+ "GITHUB_RETENTION_DAYS",
20880
+ // GitLab CI — runner-injected
20881
+ "GITLAB_CI",
20882
+ "GITLAB_USER_ID",
20883
+ "GITLAB_USER_LOGIN",
20884
+ "CI_JOB_TOKEN",
20885
+ "CI_COMMIT_SHA",
20886
+ "CI_COMMIT_REF_NAME",
20887
+ "CI_COMMIT_BRANCH",
20888
+ "CI_COMMIT_TAG",
20889
+ "CI_PROJECT_ID",
20890
+ "CI_PROJECT_PATH",
20891
+ "CI_PROJECT_NAME",
20892
+ "CI_PIPELINE_ID",
20893
+ "CI_PIPELINE_IID",
20894
+ "CI_PIPELINE_SOURCE",
20895
+ "CI_JOB_ID",
20896
+ "CI_MERGE_REQUEST_IID",
20897
+ "CI_MERGE_REQUEST_PROJECT_PATH",
20898
+ "CI_API_V4_URL",
20899
+ "CI_SERVER_URL",
20900
+ // Railway, Vercel, Netlify — platform-injected
20901
+ "RAILWAY_ENVIRONMENT",
20902
+ "RAILWAY_PROJECT_ID",
20903
+ "RAILWAY_PROJECT_NAME",
20904
+ "RAILWAY_SERVICE_ID",
20905
+ "RAILWAY_SERVICE_NAME",
20906
+ "RAILWAY_REPLICA_ID",
20907
+ "RAILWAY_STATIC_URL",
20908
+ "RAILWAY_PUBLIC_DOMAIN",
20909
+ "RAILWAY_PRIVATE_DOMAIN",
20910
+ "VERCEL",
20911
+ "VERCEL_ENV",
20912
+ "VERCEL_URL",
20913
+ "VERCEL_BRANCH_URL",
20914
+ "VERCEL_REGION",
20915
+ "VERCEL_GIT_COMMIT_SHA",
20916
+ "VERCEL_GIT_COMMIT_REF",
20917
+ "VERCEL_GIT_PROVIDER",
20918
+ "VERCEL_GIT_REPO_SLUG",
20919
+ "VERCEL_GIT_REPO_OWNER",
20920
+ "NETLIFY",
20921
+ "NETLIFY_BUILD_BASE",
20922
+ "NETLIFY_LOCAL",
20923
+ "NETLIFY_DEV",
20924
+ "CONTEXT",
20925
+ "DEPLOY_URL",
20926
+ "DEPLOY_PRIME_URL",
20927
+ "BUILD_ID",
20928
+ // Turbo internals
20929
+ "TURBO_TEAM",
20930
+ "TURBO_TOKEN",
20931
+ "TURBO_REMOTE_CACHE_SIGNATURE_KEY"
20932
+ ]);
20933
+ function isPlatformProvidedEnvVar(name) {
20934
+ if (PLATFORM_PROVIDED_ENV_VARS.has(name))
20935
+ return true;
20936
+ if (/^npm_(config_|package_|lifecycle_|execpath)/i.test(name))
20937
+ return true;
20938
+ return false;
20939
+ }
20940
+ async function detectProcessEnvTypo(input, projectRoot, policy, parsed) {
20941
+ if (policy.maintainability?.check_env_drift !== true)
20942
+ return [];
20943
+ const declared = await readDeclaredEnvVars(projectRoot);
20944
+ if (!declared)
20945
+ return [];
20946
+ const violations = [];
20947
+ for (const { path: path10, sourceFile } of parsed ?? (0, parsed_files_1.parseChangedFiles)(input)) {
20948
+ if (EXCLUDED_PATH_REGEX.test(path10))
20949
+ continue;
20950
+ scanFile(sourceFile, path10, declared, policy, violations);
20951
+ }
20952
+ return violations;
20953
+ }
20954
+ async function readDeclaredEnvVars(projectRoot) {
20955
+ const envPath = path9.join(projectRoot, ".env.example");
20956
+ let content;
20957
+ try {
20958
+ content = await fs9.promises.readFile(envPath, "utf-8");
20959
+ } catch {
20960
+ return void 0;
20961
+ }
20962
+ const declared = /* @__PURE__ */ new Set();
20963
+ for (const line of content.split("\n")) {
20964
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
20965
+ if (match)
20966
+ declared.add(match[1]);
20967
+ }
20968
+ return declared;
20969
+ }
20970
+ function scanFile(sourceFile, filePath, declared, policy, violations) {
20971
+ void policy;
20972
+ const seen = /* @__PURE__ */ new Set();
20973
+ sourceFile.forEachDescendant((node) => {
20974
+ const name = referencedEnvVar(node);
20975
+ if (!name)
20976
+ return;
20977
+ if (declared.has(name) || isPlatformProvidedEnvVar(name))
20978
+ return;
20979
+ if (seen.has(name))
20980
+ return;
20981
+ seen.add(name);
20982
+ violations.push({
20983
+ category: "maintainability",
20984
+ type: shared_1.MAINTAINABILITY_RULES.PROCESS_ENV_TYPO,
20985
+ ruleId: shared_1.MAINTAINABILITY_RULES.PROCESS_ENV_TYPO,
20986
+ severity: "warning",
20987
+ source: "deterministic",
20988
+ confidence: "medium",
20989
+ file: filePath,
20990
+ line: node.getStartLineNumber(),
20991
+ message: `Env var ${name} is referenced in code but not declared in .env.example \u2014 it is either a typo or a missing entry.`,
20992
+ suggestion: `Add ${name}= to .env.example, or correct the name in code to match a declared variable.`,
20993
+ debtPoints: 2,
20994
+ gateAction: "warn"
20995
+ });
20996
+ });
20997
+ }
20998
+ function referencedEnvVar(node) {
20999
+ if (ts_morph_1.Node.isPropertyAccessExpression(node)) {
21000
+ if (node.getExpression().getText() === "process.env") {
21001
+ return node.getName();
21002
+ }
21003
+ }
21004
+ if (ts_morph_1.Node.isElementAccessExpression(node)) {
21005
+ if (node.getExpression().getText() === "process.env") {
21006
+ const arg = node.getArgumentExpression();
21007
+ if (arg && (ts_morph_1.Node.isStringLiteral(arg) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(arg))) {
21008
+ return arg.getLiteralValue();
21009
+ }
21010
+ }
21011
+ }
21012
+ return void 0;
21013
+ }
21014
+ }
21015
+ });
21016
+
21017
+ // ../../packages/analyzers/dist/typescript/maintainability-detector.js
21018
+ var require_maintainability_detector = __commonJS({
21019
+ "../../packages/analyzers/dist/typescript/maintainability-detector.js"(exports2) {
21020
+ "use strict";
21021
+ Object.defineProperty(exports2, "__esModule", { value: true });
21022
+ exports2.detectMaintainabilityIssues = detectMaintainabilityIssues;
21023
+ var shared_1 = require_dist();
21024
+ var ts_morph_1 = require("ts-morph");
21025
+ var parsed_files_1 = require_parsed_files();
21026
+ var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|test|tests|e2e|mocks)(\/|$)/i;
21027
+ var DEFAULT_THRESHOLDS = {
21028
+ functionLength: shared_1.DEFAULT_FUNCTION_LENGTH_THRESHOLD,
21029
+ parameterCount: shared_1.DEFAULT_PARAMETER_COUNT_THRESHOLD,
21030
+ nestingDepth: shared_1.DEFAULT_NESTING_DEPTH_THRESHOLD
21031
+ };
21032
+ async function detectMaintainabilityIssues(input, policy, thresholds = {}, parsed) {
21033
+ const limits = { ...DEFAULT_THRESHOLDS, ...thresholds };
21034
+ const violations = [];
21035
+ for (const { path: path9, sourceFile } of parsed ?? (0, parsed_files_1.parseChangedFiles)(input)) {
21036
+ if (EXCLUDED_PATH_REGEX.test(path9))
21037
+ continue;
21038
+ scanFile(sourceFile, path9, limits, policy, violations);
21039
+ }
21040
+ return violations;
21041
+ }
21042
+ function scanFile(sourceFile, filePath, limits, policy, violations) {
21043
+ void policy;
21044
+ sourceFile.forEachDescendant((node) => {
21045
+ if (!isFunctionLike(node))
21046
+ return;
21047
+ const body = node.getBody();
21048
+ if (body) {
21049
+ const lineSpan = body.getEndLineNumber() - body.getStartLineNumber();
21050
+ if (lineSpan > limits.functionLength) {
21051
+ const name = getFunctionName(node);
21052
+ violations.push({
21053
+ category: "maintainability",
21054
+ type: shared_1.MAINTAINABILITY_RULES.FUNCTION_TOO_LONG,
21055
+ ruleId: shared_1.MAINTAINABILITY_RULES.FUNCTION_TOO_LONG,
21056
+ severity: "warning",
21057
+ source: "deterministic",
21058
+ confidence: "high",
21059
+ file: filePath,
21060
+ line: node.getStartLineNumber(),
21061
+ function: name,
21062
+ message: `Function '${name}' is ${lineSpan} lines long (threshold: ${limits.functionLength}) \u2014 long functions are hard to read, test, and reuse.`,
21063
+ suggestion: "Extract cohesive blocks into well-named helper functions.",
21064
+ debtPoints: 2,
21065
+ gateAction: "warn"
21066
+ });
21067
+ }
21068
+ const depth = maxNestingDepth(body);
21069
+ if (depth > limits.nestingDepth) {
21070
+ const name = getFunctionName(node);
21071
+ violations.push({
21072
+ category: "maintainability",
21073
+ type: shared_1.MAINTAINABILITY_RULES.DEEP_NESTING,
21074
+ ruleId: shared_1.MAINTAINABILITY_RULES.DEEP_NESTING,
21075
+ severity: "warning",
21076
+ source: "deterministic",
21077
+ confidence: "high",
21078
+ file: filePath,
21079
+ line: node.getStartLineNumber(),
21080
+ function: name,
21081
+ message: `Function '${name}' nests blocks ${depth} levels deep (threshold: ${limits.nestingDepth}) \u2014 deep nesting hides the control flow.`,
21082
+ suggestion: "Flatten with early returns / guard clauses, or extract inner blocks into helper functions.",
21083
+ debtPoints: 2,
21084
+ gateAction: "warn"
21085
+ });
21086
+ }
21087
+ }
21088
+ const paramCount = node.getParameters().length;
21089
+ if (paramCount > limits.parameterCount) {
21090
+ const name = getFunctionName(node);
21091
+ violations.push({
21092
+ category: "maintainability",
21093
+ type: shared_1.MAINTAINABILITY_RULES.TOO_MANY_PARAMETERS,
21094
+ ruleId: shared_1.MAINTAINABILITY_RULES.TOO_MANY_PARAMETERS,
21095
+ severity: "warning",
21096
+ source: "deterministic",
21097
+ confidence: "high",
21098
+ file: filePath,
21099
+ line: node.getStartLineNumber(),
21100
+ function: name,
21101
+ message: `Function '${name}' has ${paramCount} parameters (threshold: ${limits.parameterCount}) \u2014 long parameter lists are easy to mis-order and hard to call.`,
21102
+ suggestion: "Group related parameters into a single options object.",
21103
+ debtPoints: 2,
21104
+ gateAction: "warn"
21105
+ });
21106
+ }
21107
+ });
21108
+ }
21109
+ function isFunctionLike(node) {
21110
+ return ts_morph_1.Node.isFunctionDeclaration(node) || ts_morph_1.Node.isFunctionExpression(node) || ts_morph_1.Node.isArrowFunction(node) || ts_morph_1.Node.isMethodDeclaration(node) || ts_morph_1.Node.isConstructorDeclaration(node) || ts_morph_1.Node.isGetAccessorDeclaration(node) || ts_morph_1.Node.isSetAccessorDeclaration(node);
21111
+ }
21112
+ function getFunctionName(node) {
21113
+ if (ts_morph_1.Node.isConstructorDeclaration(node))
21114
+ return "constructor";
21115
+ if (ts_morph_1.Node.isFunctionDeclaration(node) || ts_morph_1.Node.isMethodDeclaration(node) || ts_morph_1.Node.isGetAccessorDeclaration(node) || ts_morph_1.Node.isSetAccessorDeclaration(node)) {
21116
+ return node.getName() ?? "<anonymous>";
21117
+ }
21118
+ const parent = node.getParent();
21119
+ if (ts_morph_1.Node.isVariableDeclaration(parent) || ts_morph_1.Node.isPropertyAssignment(parent)) {
21120
+ return parent.getName();
21121
+ }
21122
+ if (ts_morph_1.Node.isFunctionExpression(node)) {
21123
+ const own = node.getName();
21124
+ if (own)
21125
+ return own;
21126
+ }
21127
+ return "<anonymous>";
21128
+ }
21129
+ function maxNestingDepth(body) {
21130
+ let max = 0;
21131
+ function recurse(node, depth) {
21132
+ node.forEachChild((child) => {
21133
+ if (isFunctionLike(child))
21134
+ return;
21135
+ let childDepth = depth;
21136
+ if (isNestingNode(child)) {
21137
+ if (ts_morph_1.Node.isIfStatement(child) && isElseIf(child)) {
21138
+ childDepth = depth;
21139
+ } else {
21140
+ childDepth = depth + 1;
21141
+ if (childDepth > max)
21142
+ max = childDepth;
21143
+ }
21144
+ }
21145
+ recurse(child, childDepth);
21146
+ });
21147
+ }
21148
+ recurse(body, 0);
21149
+ return max;
21150
+ }
21151
+ function isNestingNode(node) {
21152
+ return ts_morph_1.Node.isIfStatement(node) || ts_morph_1.Node.isForStatement(node) || ts_morph_1.Node.isForInStatement(node) || ts_morph_1.Node.isForOfStatement(node) || ts_morph_1.Node.isWhileStatement(node) || ts_morph_1.Node.isDoStatement(node) || ts_morph_1.Node.isTryStatement(node) || ts_morph_1.Node.isSwitchStatement(node) || ts_morph_1.Node.isCaseClause(node) || ts_morph_1.Node.isDefaultClause(node);
21153
+ }
21154
+ function isElseIf(node) {
21155
+ const parent = node.getParent();
21156
+ return parent !== void 0 && ts_morph_1.Node.isIfStatement(parent) && parent.getElseStatement() === node;
21157
+ }
21158
+ }
21159
+ });
21160
+
21161
+ // ../../packages/analyzers/dist/typescript/console-in-production-detector.js
21162
+ var require_console_in_production_detector = __commonJS({
21163
+ "../../packages/analyzers/dist/typescript/console-in-production-detector.js"(exports2) {
21164
+ "use strict";
21165
+ Object.defineProperty(exports2, "__esModule", { value: true });
21166
+ exports2.detectConsoleInProduction = detectConsoleInProduction;
21167
+ var shared_1 = require_dist();
21168
+ var ts_morph_1 = require("ts-morph");
21169
+ var parsed_files_1 = require_parsed_files();
21170
+ var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|test|tests|e2e|mocks)(\/|$)/i;
21171
+ var ALLOW_COMMENT = "radar:allow-console";
21172
+ async function detectConsoleInProduction(input, policy, parsed) {
21173
+ const violations = [];
21174
+ for (const { path: path9, sourceFile } of parsed ?? (0, parsed_files_1.parseChangedFiles)(input)) {
21175
+ if (isExcludedConsoleFile(path9))
21176
+ continue;
21177
+ if (hasAllowConsoleComment(sourceFile.getFullText()))
21178
+ continue;
21179
+ scanFile(sourceFile, path9, policy, violations);
21180
+ }
21181
+ return violations;
21182
+ }
21183
+ function isExcludedConsoleFile(filePath) {
21184
+ if (EXCLUDED_PATH_REGEX.test(filePath))
21185
+ return true;
21186
+ if (/(^|\/)apps\/cli\/src\//.test(filePath))
21187
+ return true;
21188
+ if (/(^|\/)(scripts|build)\//.test(filePath))
21189
+ return true;
21190
+ const base = filePath.split("/").pop() ?? "";
21191
+ if (base === "index.ts" || base === "index.js")
21192
+ return true;
21193
+ return false;
21194
+ }
21195
+ function hasAllowConsoleComment(content) {
21196
+ return content.split("\n", 5).join("\n").includes(ALLOW_COMMENT);
21197
+ }
21198
+ function scanFile(sourceFile, filePath, policy, violations) {
21199
+ void policy;
21200
+ sourceFile.forEachDescendant((node) => {
21201
+ if (!ts_morph_1.Node.isCallExpression(node))
21202
+ return;
21203
+ const expr = node.getExpression();
21204
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
21205
+ return;
21206
+ if (expr.getExpression().getText() !== "console")
21207
+ return;
21208
+ const method = expr.getName();
21209
+ violations.push({
21210
+ category: "maintainability",
21211
+ type: shared_1.MAINTAINABILITY_RULES.CONSOLE_LOG_IN_PRODUCTION,
21212
+ ruleId: shared_1.MAINTAINABILITY_RULES.CONSOLE_LOG_IN_PRODUCTION,
21213
+ severity: "info",
21214
+ source: "deterministic",
21215
+ confidence: "high",
21216
+ file: filePath,
21217
+ line: node.getStartLineNumber(),
21218
+ function: getEnclosingFunctionName(node),
21219
+ message: `console.${method}() in production source \u2014 console output is unstructured and bypasses log levels and transports.`,
21220
+ suggestion: "Use the project's structured logger, or remove the call if it was a debugging leftover. Add `// radar:allow-console` at the top of the file to opt out.",
21221
+ debtPoints: 1,
21222
+ gateAction: "none"
21223
+ });
21224
+ });
21225
+ }
21226
+ function getEnclosingFunctionName(node) {
21227
+ const fn = node.getFirstAncestor((a) => ts_morph_1.Node.isFunctionDeclaration(a) || ts_morph_1.Node.isMethodDeclaration(a));
21228
+ if (fn && (ts_morph_1.Node.isFunctionDeclaration(fn) || ts_morph_1.Node.isMethodDeclaration(fn))) {
21229
+ return fn.getName() ?? void 0;
21230
+ }
21231
+ return void 0;
21232
+ }
21233
+ }
21234
+ });
21235
+
21236
+ // ../../packages/analyzers/dist/typescript/todo-comment-detector.js
21237
+ var require_todo_comment_detector = __commonJS({
21238
+ "../../packages/analyzers/dist/typescript/todo-comment-detector.js"(exports2) {
21239
+ "use strict";
21240
+ Object.defineProperty(exports2, "__esModule", { value: true });
21241
+ exports2.detectStrayTodoComments = detectStrayTodoComments;
21242
+ var shared_1 = require_dist();
21243
+ var parsed_files_1 = require_parsed_files();
21244
+ var EXCLUDED_PATH_REGEX = /(\.spec\.|\.test\.|\.d\.ts$)|(^|\/)(__tests__|__mocks__|test|tests|e2e|mocks)(\/|$)/i;
21245
+ var MARKER_REGEX = /\b(TODO|FIXME|HACK|XXX|TBD)\b\s*(\([^)]*\))?/gi;
21246
+ var TRACKER_REGEX = /#\d+|[A-Z][A-Z0-9]+-\d+|https?:\/\//;
21247
+ var LICENSE_REGEX = /SPDX-License-Identifier:|\bCopyright\b|\bLicen[sc]e\b/i;
21248
+ async function detectStrayTodoComments(input, policy, parsed) {
21249
+ const violations = [];
21250
+ for (const { path: path9, sourceFile } of parsed ?? (0, parsed_files_1.parseChangedFiles)(input)) {
21251
+ if (EXCLUDED_PATH_REGEX.test(path9))
21252
+ continue;
21253
+ scanFile(sourceFile, path9, policy, violations);
21254
+ }
21255
+ return violations;
21256
+ }
21257
+ function scanFile(sourceFile, filePath, policy, violations) {
21258
+ void policy;
21259
+ const seen = /* @__PURE__ */ new Set();
21260
+ const consider = (range) => {
21261
+ const pos = range.getPos();
21262
+ if (seen.has(pos))
21263
+ return;
21264
+ seen.add(pos);
21265
+ const keyword = strayMarkerKeyword(range.getText());
21266
+ if (!keyword)
21267
+ return;
21268
+ const { line } = sourceFile.getLineAndColumnAtPos(pos);
21269
+ violations.push({
21270
+ category: "maintainability",
21271
+ type: shared_1.MAINTAINABILITY_RULES.STRAY_TODO_COMMENT,
21272
+ ruleId: shared_1.MAINTAINABILITY_RULES.STRAY_TODO_COMMENT,
21273
+ severity: "info",
21274
+ source: "deterministic",
21275
+ confidence: "high",
21276
+ file: filePath,
21277
+ line,
21278
+ message: `Stray ${keyword} comment in committed code \u2014 unresolved markers accumulate as silent, untracked debt.`,
21279
+ suggestion: `Resolve the ${keyword}, or link it to a tracked issue, e.g. ${keyword}(#123): ....`,
21280
+ debtPoints: 1,
21281
+ gateAction: "none"
21282
+ });
21283
+ };
21284
+ sourceFile.forEachDescendant((node) => {
21285
+ for (const r of node.getLeadingCommentRanges())
21286
+ consider(r);
21287
+ for (const r of node.getTrailingCommentRanges())
21288
+ consider(r);
21289
+ });
21290
+ }
21291
+ function strayMarkerKeyword(commentText) {
21292
+ if (LICENSE_REGEX.test(commentText))
21293
+ return void 0;
21294
+ MARKER_REGEX.lastIndex = 0;
21295
+ let match;
21296
+ while ((match = MARKER_REGEX.exec(commentText)) !== null) {
21297
+ const reference = match[2] ?? "";
21298
+ if (!TRACKER_REGEX.test(reference)) {
21299
+ return match[1].toUpperCase();
21300
+ }
21301
+ }
21302
+ return void 0;
21303
+ }
21304
+ }
21305
+ });
21306
+
20375
21307
  // ../../packages/analyzers/dist/typescript/orchestrator.js
20376
21308
  var require_orchestrator = __commonJS({
20377
21309
  "../../packages/analyzers/dist/typescript/orchestrator.js"(exports2) {
@@ -20433,6 +21365,14 @@ var require_orchestrator = __commonJS({
20433
21365
  var eval_detector_1 = require_eval_detector();
20434
21366
  var math_random_detector_1 = require_math_random_detector();
20435
21367
  var tls_detector_1 = require_tls_detector();
21368
+ var cors_detector_1 = require_cors_detector();
21369
+ var jwt_detector_1 = require_jwt_detector();
21370
+ var jsx_xss_detector_1 = require_jsx_xss_detector();
21371
+ var env_typo_detector_1 = require_env_typo_detector();
21372
+ var maintainability_detector_1 = require_maintainability_detector();
21373
+ var console_in_production_detector_1 = require_console_in_production_detector();
21374
+ var todo_comment_detector_1 = require_todo_comment_detector();
21375
+ var parsed_files_1 = require_parsed_files();
20436
21376
  var EXCLUDED_FILE_PATTERNS = [
20437
21377
  /\.spec\.(ts|tsx|js|jsx)$/,
20438
21378
  /\.test\.(ts|tsx|js|jsx)$/,
@@ -20461,7 +21401,8 @@ var require_orchestrator = __commonJS({
20461
21401
  changedFiles: input.changedFiles.filter((f) => !isExcludedFile(f.path))
20462
21402
  };
20463
21403
  const projectRoot = input.projectRoot ?? deriveProjectRoot(input);
20464
- const [importGraph, complexityDeltas, runtimeResult, perfViolations, reliabilityViolations, duplicationResult, missingTestsResult, deadCodeResult, coverageDeltaResult, secretViolations, evalViolations, mathRandomViolations, tlsViolations] = await Promise.all([
21404
+ const parsedFiles = (0, parsed_files_1.parseChangedFiles)(filteredInput);
21405
+ const [importGraph, complexityDeltas, runtimeResult, perfViolations, reliabilityViolations, duplicationResult, missingTestsResult, deadCodeResult, coverageDeltaResult, secretViolations, evalViolations, mathRandomViolations, tlsViolations, corsViolations, jwtViolations, jsxXssViolations, envTypoViolations, maintainabilityViolations, consoleViolations, todoViolations] = await Promise.all([
20465
21406
  (0, import_graph_1.buildImportGraph)(filteredInput, policy),
20466
21407
  (0, complexity_calculator_1.calculateComplexity)(filteredInput),
20467
21408
  (0, runtime_risk_detector_1.detectRuntimeRisks)(filteredInput, policy),
@@ -20474,7 +21415,14 @@ var require_orchestrator = __commonJS({
20474
21415
  (0, secret_scanner_1.detectHardcodedSecrets)(filteredInput, policy),
20475
21416
  (0, eval_detector_1.detectEvalUsage)(filteredInput, policy),
20476
21417
  (0, math_random_detector_1.detectMathRandomForSecurity)(filteredInput, policy),
20477
- (0, tls_detector_1.detectTlsValidationDisabled)(filteredInput, policy)
21418
+ (0, tls_detector_1.detectTlsValidationDisabled)(filteredInput, policy),
21419
+ (0, cors_detector_1.detectCorsWildcardCredentials)(filteredInput, policy, parsedFiles),
21420
+ (0, jwt_detector_1.detectJwtInsecureOptions)(filteredInput, policy, parsedFiles),
21421
+ (0, jsx_xss_detector_1.detectDangerouslySetInnerHtml)(filteredInput, policy, parsedFiles),
21422
+ projectRoot ? (0, env_typo_detector_1.detectProcessEnvTypo)(filteredInput, projectRoot, policy, parsedFiles) : Promise.resolve([]),
21423
+ (0, maintainability_detector_1.detectMaintainabilityIssues)(filteredInput, policy, {}, parsedFiles),
21424
+ (0, console_in_production_detector_1.detectConsoleInProduction)(filteredInput, policy, parsedFiles),
21425
+ (0, todo_comment_detector_1.detectStrayTodoComments)(filteredInput, policy, parsedFiles)
20478
21426
  ]);
20479
21427
  const runtimeViolations = runtimeResult.violations;
20480
21428
  const unflaggedPatterns = runtimeResult.unflaggedPatterns;
@@ -20493,6 +21441,13 @@ var require_orchestrator = __commonJS({
20493
21441
  ...evalViolations,
20494
21442
  ...mathRandomViolations,
20495
21443
  ...tlsViolations,
21444
+ ...corsViolations,
21445
+ ...jwtViolations,
21446
+ ...jsxXssViolations,
21447
+ ...envTypoViolations,
21448
+ ...maintainabilityViolations,
21449
+ ...consoleViolations,
21450
+ ...todoViolations,
20496
21451
  ...boundaryViolations,
20497
21452
  ...circularViolations,
20498
21453
  ...runtimeViolations,
@@ -20837,7 +21792,7 @@ var require_dist3 = __commonJS({
20837
21792
  "../../packages/analyzers/dist/index.js"(exports2) {
20838
21793
  "use strict";
20839
21794
  Object.defineProperty(exports2, "__esModule", { value: true });
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;
21795
+ exports2.runFullAnalysis = exports2.detectStrayTodoComments = exports2.detectConsoleInProduction = exports2.detectMaintainabilityIssues = exports2.detectProcessEnvTypo = exports2.detectDangerouslySetInnerHtml = exports2.detectJwtInsecureOptions = exports2.detectCorsWildcardCredentials = 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;
20841
21796
  var import_graph_1 = require_import_graph();
20842
21797
  Object.defineProperty(exports2, "buildImportGraph", { enumerable: true, get: function() {
20843
21798
  return import_graph_1.buildImportGraph;
@@ -20909,6 +21864,34 @@ var require_dist3 = __commonJS({
20909
21864
  Object.defineProperty(exports2, "detectTlsValidationDisabled", { enumerable: true, get: function() {
20910
21865
  return tls_detector_1.detectTlsValidationDisabled;
20911
21866
  } });
21867
+ var cors_detector_1 = require_cors_detector();
21868
+ Object.defineProperty(exports2, "detectCorsWildcardCredentials", { enumerable: true, get: function() {
21869
+ return cors_detector_1.detectCorsWildcardCredentials;
21870
+ } });
21871
+ var jwt_detector_1 = require_jwt_detector();
21872
+ Object.defineProperty(exports2, "detectJwtInsecureOptions", { enumerable: true, get: function() {
21873
+ return jwt_detector_1.detectJwtInsecureOptions;
21874
+ } });
21875
+ var jsx_xss_detector_1 = require_jsx_xss_detector();
21876
+ Object.defineProperty(exports2, "detectDangerouslySetInnerHtml", { enumerable: true, get: function() {
21877
+ return jsx_xss_detector_1.detectDangerouslySetInnerHtml;
21878
+ } });
21879
+ var env_typo_detector_1 = require_env_typo_detector();
21880
+ Object.defineProperty(exports2, "detectProcessEnvTypo", { enumerable: true, get: function() {
21881
+ return env_typo_detector_1.detectProcessEnvTypo;
21882
+ } });
21883
+ var maintainability_detector_1 = require_maintainability_detector();
21884
+ Object.defineProperty(exports2, "detectMaintainabilityIssues", { enumerable: true, get: function() {
21885
+ return maintainability_detector_1.detectMaintainabilityIssues;
21886
+ } });
21887
+ var console_in_production_detector_1 = require_console_in_production_detector();
21888
+ Object.defineProperty(exports2, "detectConsoleInProduction", { enumerable: true, get: function() {
21889
+ return console_in_production_detector_1.detectConsoleInProduction;
21890
+ } });
21891
+ var todo_comment_detector_1 = require_todo_comment_detector();
21892
+ Object.defineProperty(exports2, "detectStrayTodoComments", { enumerable: true, get: function() {
21893
+ return todo_comment_detector_1.detectStrayTodoComments;
21894
+ } });
20912
21895
  var orchestrator_1 = require_orchestrator();
20913
21896
  Object.defineProperty(exports2, "runFullAnalysis", { enumerable: true, get: function() {
20914
21897
  return orchestrator_1.runFullAnalysis;
@@ -20921,8 +21904,8 @@ var require_package = __commonJS({
20921
21904
  "package.json"(exports2, module2) {
20922
21905
  module2.exports = {
20923
21906
  name: "technical-debt-radar",
20924
- version: "1.16.0",
20925
- description: "Stop Node.js production crashes before merge. 65 detection rules across 5 categories.",
21907
+ version: "1.17.1",
21908
+ description: "Stop Node.js production crashes before merge. 74 detection rules across 5 categories.",
20926
21909
  bin: {
20927
21910
  radar: "dist/index.js",
20928
21911
  "technical-debt-radar": "dist/index.js"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.16.0",
4
- "description": "Stop Node.js production crashes before merge. 65 detection rules across 5 categories.",
3
+ "version": "1.17.1",
4
+ "description": "Stop Node.js production crashes before merge. 74 detection rules across 5 categories.",
5
5
  "bin": {
6
6
  "radar": "dist/index.js",
7
7
  "technical-debt-radar": "dist/index.js"