@critiq/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.js CHANGED
@@ -56,22 +56,22 @@ function createJsonPointer(segments) {
56
56
  }
57
57
  return `/${segments.map(escapeJsonPointerSegment).join("/")}`;
58
58
  }
59
- function createDiagnostic(input) {
60
- assertNonEmptyString(String(input.code), "code");
61
- assertNonEmptyString(input.message, "message");
62
- const severity = input.severity ?? DIAGNOSTIC_SEVERITY_ERROR;
59
+ function createDiagnostic(input2) {
60
+ assertNonEmptyString(String(input2.code), "code");
61
+ assertNonEmptyString(input2.message, "message");
62
+ const severity = input2.severity ?? DIAGNOSTIC_SEVERITY_ERROR;
63
63
  assertSeverity(severity);
64
- if (input.summary !== void 0) {
65
- assertNonEmptyString(input.summary, "summary");
64
+ if (input2.summary !== void 0) {
65
+ assertNonEmptyString(input2.summary, "summary");
66
66
  }
67
67
  return {
68
- code: input.code,
68
+ code: input2.code,
69
69
  severity,
70
- message: input.message,
71
- summary: input.summary,
72
- sourceSpan: input.sourceSpan,
73
- jsonPointer: input.jsonPointer,
74
- details: input.details
70
+ message: input2.message,
71
+ summary: input2.summary,
72
+ sourceSpan: input2.sourceSpan,
73
+ jsonPointer: input2.jsonPointer,
74
+ details: input2.details
75
75
  };
76
76
  }
77
77
  function compareNullableStrings(left, right) {
@@ -142,7 +142,7 @@ function formatDiagnosticsForTerminal(diagnostics) {
142
142
  }
143
143
 
144
144
  // apps/cli/src/main.ts
145
- var import_node_path15 = require("node:path");
145
+ var import_node_path22 = require("node:path");
146
146
 
147
147
  // apps/cli/src/cli-runtime.ts
148
148
  var defaultRuntime = {
@@ -167,7 +167,10 @@ function resolveRuntime(runtime = {}) {
167
167
  writeStdout: runtime.writeStdout ?? defaultRuntime.writeStdout,
168
168
  writeStderr: runtime.writeStderr ?? defaultRuntime.writeStderr,
169
169
  writeRaw: runtime.writeRaw ?? (runtime.writeStdout || runtime.writeStderr ? discardOutput : defaultRuntime.writeRaw),
170
- isInteractive: runtime.isInteractive ?? (runtime.writeRaw ? true : runtime.writeStdout || runtime.writeStderr ? false : defaultRuntime.isInteractive)
170
+ isInteractive: runtime.isInteractive ?? (runtime.writeRaw ? true : runtime.writeStdout || runtime.writeStderr ? false : defaultRuntime.isInteractive),
171
+ cliInstallScope: runtime.cliInstallScope,
172
+ promptChoice: runtime.promptChoice,
173
+ runPackageInstall: runtime.runPackageInstall
171
174
  };
172
175
  }
173
176
 
@@ -366,8 +369,8 @@ var ruleCatalogSchema = import_zod.z.object({
366
369
  }).strict()
367
370
  ).min(1)
368
371
  }).strict();
369
- function validateRuleCatalog(input) {
370
- const result = ruleCatalogSchema.safeParse(input);
372
+ function validateRuleCatalog(input2) {
373
+ const result = ruleCatalogSchema.safeParse(input2);
371
374
  if (result.success) {
372
375
  return {
373
376
  success: true,
@@ -415,20 +418,20 @@ function loadRuleCatalogText(text, path) {
415
418
  const uri = (0, import_node_url.pathToFileURL)(path).href;
416
419
  const loaded = loadYamlText(text, uri);
417
420
  if (!loaded.success) {
418
- const failure = loaded;
421
+ const failure2 = loaded;
419
422
  return {
420
423
  success: false,
421
- diagnostics: failure.issues.map(issueToDiagnostic),
424
+ diagnostics: failure2.issues.map(issueToDiagnostic),
422
425
  path,
423
426
  uri
424
427
  };
425
428
  }
426
429
  const validated = validateRuleCatalog(loaded.data);
427
430
  if (!validated.success) {
428
- const failure = validated;
431
+ const failure2 = validated;
429
432
  return {
430
433
  success: false,
431
- diagnostics: failure.diagnostics,
434
+ diagnostics: failure2.diagnostics,
432
435
  path,
433
436
  uri
434
437
  };
@@ -506,7 +509,7 @@ function resolveCatalogPackage(cwd, packageName, additionalResolverBasePaths = [
506
509
  diagnostics: [
507
510
  createDiagnostic({
508
511
  code: "catalog.package.not-found",
509
- message: `Unable to resolve catalog package \`${packageName}\`.`,
512
+ message: `Unable to resolve catalog package \`${packageName}\`. Install it in this repository or globally, then run \`critiq check\` again.`,
510
513
  details: {
511
514
  packageName
512
515
  }
@@ -531,6 +534,18 @@ function resolveCatalogRulePaths(catalog, packageRoot, preset) {
531
534
  rulePath: (0, import_node_path.resolve)(packageRoot, entry.rulePath)
532
535
  }));
533
536
  }
537
+ function looksLikeCloudFormationPath(filePath) {
538
+ const normalized = filePath.replace(/\\/gu, "/").toLowerCase();
539
+ const fileName = normalized.split("/").pop() ?? normalized;
540
+ if (/(?:^|\/)(?:templates?|cloudformation|cfn|sam|infra|iac)(?:\/|$)/u.test(
541
+ normalized
542
+ )) {
543
+ return true;
544
+ }
545
+ return /(?:template|cloudformation|stack|cfn|sam)[^/]*\.(?:ya?ml|json)$/u.test(
546
+ fileName
547
+ );
548
+ }
534
549
  function detectRepositoryLanguages(filePaths) {
535
550
  const detected = /* @__PURE__ */ new Set();
536
551
  for (const filePath of filePaths) {
@@ -559,6 +574,9 @@ function detectRepositoryLanguages(filePaths) {
559
574
  if (extension === ".rs") {
560
575
  detected.add("rust");
561
576
  }
577
+ if ((extension === ".yaml" || extension === ".yml" || extension === ".json") && looksLikeCloudFormationPath(filePath)) {
578
+ detected.add("cloudformation");
579
+ }
562
580
  }
563
581
  return [...detected].sort();
564
582
  }
@@ -713,8 +731,8 @@ function normalizeZodIssue(issue) {
713
731
  }
714
732
  return normalized;
715
733
  }
716
- function validateFinding(input) {
717
- const result = findingV0Schema.safeParse(input);
734
+ function validateFinding(input2) {
735
+ const result = findingV0Schema.safeParse(input2);
718
736
  if (result.success) {
719
737
  return {
720
738
  success: true,
@@ -744,7 +762,8 @@ var configLanguageSchema = import_zod4.z.enum([
744
762
  "java",
745
763
  "php",
746
764
  "ruby",
747
- "rust"
765
+ "rust",
766
+ "cloudformation"
748
767
  ]);
749
768
  var CRITIQ_CONFIG_DEFAULT_PATH = ".critiq/config.yaml";
750
769
  var critiqPresetSchema = import_zod4.z.enum([
@@ -866,8 +885,8 @@ function normalizeCritiqConfig(config) {
866
885
  secretsScan: normalizeSecretsScanConfig(config.secretsScan)
867
886
  };
868
887
  }
869
- function validateCritiqConfig(input) {
870
- const result = critiqConfigSchema.safeParse(input);
888
+ function validateCritiqConfig(input2) {
889
+ const result = critiqConfigSchema.safeParse(input2);
871
890
  if (result.success) {
872
891
  return {
873
892
  success: true,
@@ -889,20 +908,20 @@ function loadCritiqConfigText(text, path) {
889
908
  const uri = (0, import_node_url2.pathToFileURL)(path).href;
890
909
  const loaded = loadYamlText(text, uri);
891
910
  if (!loaded.success) {
892
- const failure = loaded;
911
+ const failure2 = loaded;
893
912
  return {
894
913
  success: false,
895
- diagnostics: failure.issues.map(issueToDiagnostic2),
914
+ diagnostics: failure2.issues.map(issueToDiagnostic2),
896
915
  path,
897
916
  uri
898
917
  };
899
918
  }
900
919
  const validated = validateCritiqConfig(loaded.data);
901
920
  if (!validated.success) {
902
- const failure = validated;
921
+ const failure2 = validated;
903
922
  return {
904
923
  success: false,
905
- diagnostics: failure.diagnostics,
924
+ diagnostics: failure2.diagnostics,
906
925
  path,
907
926
  uri
908
927
  };
@@ -1721,10 +1740,10 @@ function buildFinding(rule, analyzedFile, match, options = {}) {
1721
1740
  };
1722
1741
  const validation = validateFinding(finding);
1723
1742
  if (!validation.success) {
1724
- const failure = validation;
1743
+ const failure2 = validation;
1725
1744
  return {
1726
1745
  success: false,
1727
- issues: toFindingIssues(failure.issues)
1746
+ issues: toFindingIssues(failure2.issues)
1728
1747
  };
1729
1748
  }
1730
1749
  return {
@@ -1734,8 +1753,8 @@ function buildFinding(rule, analyzedFile, match, options = {}) {
1734
1753
  }
1735
1754
 
1736
1755
  // libs/runtime/check-runner/src/lib/check-runner/runtime.ts
1737
- var import_node_fs8 = require("node:fs");
1738
- var import_node_path10 = require("node:path");
1756
+ var import_node_fs9 = require("node:fs");
1757
+ var import_node_path12 = require("node:path");
1739
1758
 
1740
1759
  // libs/runtime/check-runner/src/lib/check-runner/findings.ts
1741
1760
  function applySeverityOverride(finding, severityOverride) {
@@ -3220,6 +3239,9 @@ var RUBY_RAILS_SECURITY_FACT_KINDS = {
3220
3239
  csrfDisabled: "ruby.security.rails-csrf-disabled",
3221
3240
  openRedirect: "ruby.security.rails-open-redirect",
3222
3241
  unsafeHtmlOutput: "ruby.security.rails-unsafe-html-output",
3242
+ plaintextPasswordInCallback: "ruby.security.plaintext-password-in-callback",
3243
+ linkToBlankWithoutNoopener: "ruby.security.rails-link-to-blank-without-noopener",
3244
+ railsOutputUnsafe: "ruby.security.rails-output-unsafe",
3223
3245
  sidekiqWebUnauthenticatedMount: "ruby.security.sidekiq-web-unauthenticated-mount",
3224
3246
  unsafeRender: "ruby.security.rails-unsafe-render",
3225
3247
  detailedExceptionsEnabled: "ruby.security.rails-detailed-exceptions-enabled",
@@ -3274,7 +3296,10 @@ function collectRubyRailsSecurityFacts(options) {
3274
3296
  ...collectSidekiqMountFacts(text, detector),
3275
3297
  ...collectUnsafeRenderFacts(text, detector, state, matchesTainted),
3276
3298
  ...collectDetailedExceptionsFacts(text, detector, path),
3277
- ...collectUnsafeSessionCookieFacts(text, detector)
3299
+ ...collectUnsafeSessionCookieFacts(text, detector),
3300
+ ...collectPlaintextPasswordInCallbackFacts(text, detector),
3301
+ ...collectLinkToBlankWithoutNoopenerFacts(text, detector),
3302
+ ...collectRailsOutputUnsafeFacts(text, path, detector)
3278
3303
  ];
3279
3304
  }
3280
3305
  function collectUnsafeStrongParametersFacts(text, detector) {
@@ -3551,6 +3576,89 @@ function collectDetailedExceptionsFacts(text, detector, path) {
3551
3576
  })
3552
3577
  );
3553
3578
  }
3579
+ var RAILS_HASH_CALL_WINDOW = 420;
3580
+ function sliceRailsHashCallWindow(text, startOffset, endOffset) {
3581
+ return text.slice(
3582
+ startOffset,
3583
+ Math.min(text.length, endOffset + RAILS_HASH_CALL_WINDOW)
3584
+ );
3585
+ }
3586
+ function collectPlaintextPasswordInCallbackFacts(text, detector) {
3587
+ const kind = RUBY_RAILS_SECURITY_FACT_KINDS.plaintextPasswordInCallback;
3588
+ return collectMatchedFacts({
3589
+ text,
3590
+ detector,
3591
+ kind,
3592
+ appliesTo: "block",
3593
+ pattern: /\bhttp_basic_authenticate_with\b/g,
3594
+ predicate: (match) => {
3595
+ const window = sliceRailsHashCallWindow(
3596
+ text,
3597
+ match.startOffset,
3598
+ match.endOffset
3599
+ );
3600
+ if (!/password:\s*["']/u.test(window)) {
3601
+ return false;
3602
+ }
3603
+ if (/password:\s*ENV(?:\.fetch)?\b/u.test(window)) {
3604
+ return false;
3605
+ }
3606
+ if (/password:\s*Rails\.application\.credentials/u.test(window)) {
3607
+ return false;
3608
+ }
3609
+ return true;
3610
+ }
3611
+ });
3612
+ }
3613
+ function collectLinkToBlankWithoutNoopenerFacts(text, detector) {
3614
+ const kind = RUBY_RAILS_SECURITY_FACT_KINDS.linkToBlankWithoutNoopener;
3615
+ return collectMatchedFacts({
3616
+ text,
3617
+ detector,
3618
+ kind,
3619
+ appliesTo: "block",
3620
+ pattern: /\blink_to\b/g,
3621
+ predicate: (match) => {
3622
+ const window = sliceRailsHashCallWindow(
3623
+ text,
3624
+ match.startOffset,
3625
+ match.endOffset
3626
+ );
3627
+ if (!/target:\s*['"]_blank['"]/u.test(window)) {
3628
+ return false;
3629
+ }
3630
+ return !/rel:\s*['"][^'"]*(?:noopener|noreferrer)/u.test(window);
3631
+ }
3632
+ });
3633
+ }
3634
+ function collectRailsOutputUnsafeFacts(text, path, detector) {
3635
+ if (isTestLikeSourcePath(path)) {
3636
+ return [];
3637
+ }
3638
+ const kind = RUBY_RAILS_SECURITY_FACT_KINDS.railsOutputUnsafe;
3639
+ return collectMatchedFacts({
3640
+ text,
3641
+ detector,
3642
+ kind,
3643
+ appliesTo: "block",
3644
+ pattern: /\.html_safe\b/g
3645
+ }).concat(
3646
+ collectMatchedFacts({
3647
+ text,
3648
+ detector,
3649
+ kind,
3650
+ appliesTo: "block",
3651
+ pattern: /\braw\s*\(/g
3652
+ }),
3653
+ collectMatchedFacts({
3654
+ text,
3655
+ detector,
3656
+ kind,
3657
+ appliesTo: "block",
3658
+ pattern: /\.safe_concat\s*\(/g
3659
+ })
3660
+ );
3661
+ }
3554
3662
  function collectUnsafeSessionCookieFacts(text, detector) {
3555
3663
  const kind = RUBY_RAILS_SECURITY_FACT_KINDS.unsafeSessionOrCookieStore;
3556
3664
  return collectMatchedFacts({
@@ -3584,6 +3692,234 @@ function collectRubySensitiveDataEgressFacts(options) {
3584
3692
  });
3585
3693
  }
3586
3694
 
3695
+ // libs/adapters/shared/src/lib/polyglot/domains/ruby-general-security.ts
3696
+ var RUBY_GENERAL_SECURITY_FACT_KINDS = {
3697
+ dynamicCodeExecution: "ruby.security.dynamic-code-execution",
3698
+ kernelOpen: "ruby.security.kernel-open",
3699
+ insecureJsonLoad: "ruby.security.insecure-json-load",
3700
+ debuggerCall: "ruby.security.debugger-call"
3701
+ };
3702
+ function collectRubyGeneralSecurityFacts(options) {
3703
+ const { text, path, detector } = options;
3704
+ return [
3705
+ ...collectDynamicCodeExecutionFacts2(text, detector),
3706
+ ...collectKernelOpenFacts(text, detector),
3707
+ ...collectInsecureJsonLoadFacts(text, detector),
3708
+ ...collectDebuggerCallFacts(text, path, detector)
3709
+ ];
3710
+ }
3711
+ function collectDynamicCodeExecutionFacts2(text, detector) {
3712
+ return collectMatchedFacts({
3713
+ text,
3714
+ detector,
3715
+ kind: RUBY_GENERAL_SECURITY_FACT_KINDS.dynamicCodeExecution,
3716
+ appliesTo: "block",
3717
+ pattern: /\b(?:eval|exec|binding\.eval|class_eval|module_eval|instance_eval)\s*[({]/g
3718
+ });
3719
+ }
3720
+ function collectKernelOpenFacts(text, detector) {
3721
+ return collectMatchedFacts({
3722
+ text,
3723
+ detector,
3724
+ kind: RUBY_GENERAL_SECURITY_FACT_KINDS.kernelOpen,
3725
+ appliesTo: "block",
3726
+ pattern: /\b(?:Kernel\.)?open\s*\(\s*["'][|]/g
3727
+ });
3728
+ }
3729
+ function collectInsecureJsonLoadFacts(text, detector) {
3730
+ return collectMatchedFacts({
3731
+ text,
3732
+ detector,
3733
+ kind: RUBY_GENERAL_SECURITY_FACT_KINDS.insecureJsonLoad,
3734
+ appliesTo: "block",
3735
+ pattern: /\b(?:JSON\.(?:load|restore)|Oj\.load|MultiJson\.load)\s*\(/g
3736
+ });
3737
+ }
3738
+ function collectDebuggerCallFacts(text, path, detector) {
3739
+ if (isTestLikeSourcePath(path)) {
3740
+ return [];
3741
+ }
3742
+ return collectMatchedFacts({
3743
+ text,
3744
+ detector,
3745
+ kind: RUBY_GENERAL_SECURITY_FACT_KINDS.debuggerCall,
3746
+ appliesTo: "block",
3747
+ pattern: /\b(?:debugger|byebug|binding\.break)\b/g
3748
+ });
3749
+ }
3750
+
3751
+ // libs/adapters/shared/src/lib/polyglot/domains/ruby-bug-risk.ts
3752
+ var RUBY_BUG_RISK_FACT_KINDS = {
3753
+ exceptionClassOverwritten: "ruby.bug-risk.exception-class-overwritten",
3754
+ rawSqlWithoutSquish: "ruby.bug-risk.raw-sql-without-squish",
3755
+ divisionByZero: "ruby.bug-risk.division-by-zero",
3756
+ assignmentInCondition: "ruby.bug-risk.assignment-in-condition",
3757
+ duplicateHashKeys: "ruby.bug-risk.duplicate-hash-keys",
3758
+ deprecatedUriEscape: "ruby.bug-risk.deprecated-uri-escape"
3759
+ };
3760
+ var RESCUE_EXCEPTION_CLASS_NAMES = "StandardError|Exception|RuntimeError|ArgumentError|NameError|TypeError|NoMethodError|IOError|IndexError|RangeError|RegexpError|SyntaxError|LoadError|ZeroDivisionError|NotImplementedError|ScriptError|SecurityError|SystemCallError|SystemStackError|ThreadError|FrozenError|LocalJumpError";
3761
+ function collectRubyBugRiskFacts(options) {
3762
+ const { text, detector } = options;
3763
+ return dedupeFacts([
3764
+ ...collectExceptionClassOverwrittenFacts(text, detector),
3765
+ ...collectRawSqlWithoutSquishFacts(text, detector),
3766
+ ...collectDivisionByZeroFacts(text, detector),
3767
+ ...collectAssignmentInConditionFacts(text, detector),
3768
+ ...collectDuplicateHashKeyFacts(text, detector),
3769
+ ...collectDeprecatedUriEscapeFacts(text, detector)
3770
+ ]);
3771
+ }
3772
+ function collectExceptionClassOverwrittenFacts(text, detector) {
3773
+ return collectMatchedFacts({
3774
+ text,
3775
+ detector,
3776
+ kind: RUBY_BUG_RISK_FACT_KINDS.exceptionClassOverwritten,
3777
+ appliesTo: "block",
3778
+ pattern: new RegExp(
3779
+ `\\brescue\\s*=>\\s*(?:${RESCUE_EXCEPTION_CLASS_NAMES})\\b`,
3780
+ "g"
3781
+ )
3782
+ });
3783
+ }
3784
+ function collectRawSqlWithoutSquishFacts(text, detector) {
3785
+ const kind = RUBY_BUG_RISK_FACT_KINDS.rawSqlWithoutSquish;
3786
+ const callPatterns = [/\bwhere\s*\(/g, /\bfind_by_sql\s*\(/g];
3787
+ return callPatterns.flatMap(
3788
+ (pattern) => collectSnippetFacts({
3789
+ text,
3790
+ detector,
3791
+ kind,
3792
+ appliesTo: "block",
3793
+ pattern,
3794
+ state: void 0,
3795
+ predicate: (snippet) => {
3796
+ if (!/<<-?~?\w*/u.test(snippet.text)) {
3797
+ return false;
3798
+ }
3799
+ return !/\.squish\b/u.test(snippet.text);
3800
+ }
3801
+ })
3802
+ );
3803
+ }
3804
+ function collectDivisionByZeroFacts(text, detector) {
3805
+ const kind = RUBY_BUG_RISK_FACT_KINDS.divisionByZero;
3806
+ const facts = [];
3807
+ const lines = text.split("\n");
3808
+ let offset = 0;
3809
+ for (const line of lines) {
3810
+ const stripped = stripHashLineComment(line);
3811
+ const pattern = /\/\s*0(?:\.0+)?\b/g;
3812
+ for (const match of findAllMatches(stripped, pattern)) {
3813
+ facts.push(
3814
+ createOffsetFact(text, {
3815
+ detector,
3816
+ appliesTo: "block",
3817
+ kind,
3818
+ startOffset: offset + match.startOffset,
3819
+ endOffset: offset + match.endOffset,
3820
+ text: match.matchedText
3821
+ })
3822
+ );
3823
+ }
3824
+ offset += line.length + 1;
3825
+ }
3826
+ return facts;
3827
+ }
3828
+ function collectAssignmentInConditionFacts(text, detector) {
3829
+ const kind = RUBY_BUG_RISK_FACT_KINDS.assignmentInCondition;
3830
+ return collectMatchedFacts({
3831
+ text,
3832
+ detector,
3833
+ kind,
3834
+ appliesTo: "block",
3835
+ pattern: /\b(?:if|unless|while|until)\s+(?:\([^)]*\s*)?(\w+)\s*=(?!=)(?![=>])/g
3836
+ });
3837
+ }
3838
+ function collectDeprecatedUriEscapeFacts(text, detector) {
3839
+ return collectMatchedFacts({
3840
+ text,
3841
+ detector,
3842
+ kind: RUBY_BUG_RISK_FACT_KINDS.deprecatedUriEscape,
3843
+ appliesTo: "block",
3844
+ pattern: /\bURI\.(?:escape|unescape|encode|decode)\s*\(/g
3845
+ });
3846
+ }
3847
+ function collectDuplicateHashKeyFacts(text, detector) {
3848
+ const kind = RUBY_BUG_RISK_FACT_KINDS.duplicateHashKeys;
3849
+ const findings = [];
3850
+ for (const literal of collectRubyHashLiteralRanges(text)) {
3851
+ const seen = /* @__PURE__ */ new Set();
3852
+ for (const key of extractRubyHashKeys(literal.content)) {
3853
+ if (seen.has(key.normalized)) {
3854
+ findings.push(
3855
+ createOffsetFact(text, {
3856
+ detector,
3857
+ appliesTo: "block",
3858
+ kind,
3859
+ startOffset: literal.startOffset + key.startOffset,
3860
+ endOffset: literal.startOffset + key.endOffset,
3861
+ text: key.raw
3862
+ })
3863
+ );
3864
+ continue;
3865
+ }
3866
+ seen.add(key.normalized);
3867
+ }
3868
+ }
3869
+ return findings;
3870
+ }
3871
+ function collectRubyHashLiteralRanges(text) {
3872
+ const ranges = [];
3873
+ const stack = [];
3874
+ for (let index = 0; index < text.length; index += 1) {
3875
+ const char = text[index];
3876
+ if (char === "{") {
3877
+ stack.push(index);
3878
+ continue;
3879
+ }
3880
+ if (char !== "}" || stack.length === 0) {
3881
+ continue;
3882
+ }
3883
+ const start = stack.pop();
3884
+ if (start === void 0) {
3885
+ continue;
3886
+ }
3887
+ const content = text.slice(start + 1, index);
3888
+ if (!looksLikeRubyHashLiteral(content)) {
3889
+ continue;
3890
+ }
3891
+ ranges.push({
3892
+ startOffset: start,
3893
+ endOffset: index + 1,
3894
+ content
3895
+ });
3896
+ }
3897
+ return ranges;
3898
+ }
3899
+ function looksLikeRubyHashLiteral(content) {
3900
+ return /:\w+\s*=>|["'][\w-]+["']\s*=>|\b\w+\s*:/u.test(content);
3901
+ }
3902
+ function extractRubyHashKeys(hashContent) {
3903
+ const keys = [];
3904
+ for (const match of hashContent.matchAll(
3905
+ /(?:(["'])([^"']+)\1\s*=>)|(?::(\w+)\s*=>)|(?:\b(\w+)\s*:)/gu
3906
+ )) {
3907
+ const symbol = match[2] ?? match[3] ?? match[4];
3908
+ if (!symbol) {
3909
+ continue;
3910
+ }
3911
+ const raw = match[0];
3912
+ const startOffset = match.index ?? 0;
3913
+ keys.push({
3914
+ raw,
3915
+ normalized: symbol.toLowerCase(),
3916
+ startOffset,
3917
+ endOffset: startOffset + raw.length
3918
+ });
3919
+ }
3920
+ return keys;
3921
+ }
3922
+
3587
3923
  // libs/adapters/shared/src/lib/polyglot/domains/php-framework-security.ts
3588
3924
  var PHP_FRAMEWORK_SECURITY_FACT_KINDS = {
3589
3925
  laravelUnsafeMassAssignment: "php.security.laravel-unsafe-mass-assignment",
@@ -3870,8 +4206,60 @@ var PHP_CORRECTNESS_FACT_KINDS = {
3870
4206
  switchMultipleDefault: "php.correctness.switch-multiple-default",
3871
4207
  errorSuppressionOperator: "php.correctness.error-suppression-operator",
3872
4208
  unreachableAfterReturn: "php.correctness.unreachable-after-return",
3873
- nullsafeReturnedByReference: "php.correctness.nullsafe-returned-by-reference"
4209
+ nullsafeReturnedByReference: "php.correctness.nullsafe-returned-by-reference",
4210
+ emptyArrayLiteralSlot: "php.correctness.empty-array-literal-slot",
4211
+ emptyBracketArrayAccess: "php.correctness.empty-bracket-array-access",
4212
+ deprecatedUnsetCast: "php.correctness.deprecated-unset-cast",
4213
+ duplicateDeclaration: "php.correctness.duplicate-declaration",
4214
+ nestedFunctionDeclaration: "php.correctness.nested-function-declaration",
4215
+ breakContinueOutsideLoop: "php.correctness.break-continue-outside-loop",
4216
+ abstractMethodOutsideAbstractClass: "php.correctness.abstract-method-outside-abstract-class",
4217
+ uselessUnset: "php.correctness.useless-unset",
4218
+ invalidRegexLiteral: "php.correctness.invalid-regex-literal",
4219
+ todoFixmeMarker: "php.correctness.todo-fixme-marker",
4220
+ selfAssignment: "php.correctness.self-assignment",
4221
+ defaultParameterNotLast: "php.correctness.default-parameter-not-last",
4222
+ emptyFunctionBody: "php.correctness.empty-function-body",
4223
+ unknownMagicMethod: "php.correctness.unknown-magic-method",
4224
+ caseInsensitiveDefine: "php.correctness.case-insensitive-define",
4225
+ deprecatedFilterConstant: "php.correctness.deprecated-filter-constant",
4226
+ emptyCodeBlock: "php.correctness.empty-code-block",
4227
+ deprecatedLibxmlEntityLoader: "php.correctness.deprecated-libxml-entity-loader",
4228
+ redundantStringCastConcat: "php.correctness.redundant-string-cast-concat",
4229
+ missingMemberVisibility: "php.correctness.missing-member-visibility",
4230
+ functionComparison: "php.correctness.function-comparison",
4231
+ uselessPostIncrement: "php.correctness.useless-post-increment",
4232
+ nestedSwitch: "php.correctness.nested-switch",
4233
+ invalidCookieOptions: "php.correctness.invalid-cookie-options"
3874
4234
  };
4235
+ var PHP_VALID_MAGIC_METHODS = /* @__PURE__ */ new Set([
4236
+ "__construct",
4237
+ "__destruct",
4238
+ "__call",
4239
+ "__callStatic",
4240
+ "__get",
4241
+ "__set",
4242
+ "__isset",
4243
+ "__unset",
4244
+ "__sleep",
4245
+ "__wakeup",
4246
+ "__serialize",
4247
+ "__unserialize",
4248
+ "__toString",
4249
+ "__invoke",
4250
+ "__set_state",
4251
+ "__clone",
4252
+ "__debugInfo"
4253
+ ]);
4254
+ var PHP_VALID_COOKIE_OPTION_KEYS = /* @__PURE__ */ new Set([
4255
+ "expires",
4256
+ "path",
4257
+ "domain",
4258
+ "secure",
4259
+ "httponly",
4260
+ "samesite"
4261
+ ]);
4262
+ var PHP_DEPRECATED_FILTER_CONSTANTS = /\bFILTER_(?:SANITIZE_STRING|SANITIZE_MAGIC_QUOTES|FLAG_ALLOW_THOUSAND)\b/gu;
3875
4263
  function collectPhpCorrectnessFacts(options) {
3876
4264
  const { text, detector } = options;
3877
4265
  return dedupeFacts([
@@ -3879,7 +4267,31 @@ function collectPhpCorrectnessFacts(options) {
3879
4267
  ...collectSwitchMultipleDefaultFacts(text, detector),
3880
4268
  ...collectErrorSuppressionOperatorFacts(text, detector),
3881
4269
  ...collectUnreachableAfterReturnFacts(text, detector),
3882
- ...collectNullsafeReturnedByReferenceFacts(text, detector)
4270
+ ...collectNullsafeReturnedByReferenceFacts(text, detector),
4271
+ ...collectEmptyArrayLiteralSlotFacts(text, detector),
4272
+ ...collectEmptyBracketArrayAccessFacts(text, detector),
4273
+ ...collectDeprecatedUnsetCastFacts(text, detector),
4274
+ ...collectDuplicateDeclarationFacts(text, detector),
4275
+ ...collectNestedFunctionDeclarationFacts(text, detector),
4276
+ ...collectBreakContinueOutsideLoopFacts(text, detector),
4277
+ ...collectAbstractMethodOutsideAbstractClassFacts(text, detector),
4278
+ ...collectUselessUnsetFacts(text, detector),
4279
+ ...collectInvalidRegexLiteralFacts(text, detector),
4280
+ ...collectTodoFixmeMarkerFacts(text, detector),
4281
+ ...collectSelfAssignmentFacts(text, detector),
4282
+ ...collectDefaultParameterNotLastFacts(text, detector),
4283
+ ...collectEmptyFunctionBodyFacts(text, detector),
4284
+ ...collectUnknownMagicMethodFacts(text, detector),
4285
+ ...collectCaseInsensitiveDefineFacts(text, detector),
4286
+ ...collectDeprecatedFilterConstantFacts(text, detector),
4287
+ ...collectEmptyCodeBlockFacts(text, detector),
4288
+ ...collectDeprecatedLibxmlEntityLoaderFacts(text, detector),
4289
+ ...collectRedundantStringCastConcatFacts(text, detector),
4290
+ ...collectMissingMemberVisibilityFacts(text, detector),
4291
+ ...collectFunctionComparisonFacts(text, detector),
4292
+ ...collectUselessPostIncrementFacts(text, detector),
4293
+ ...collectNestedSwitchFacts(text, detector),
4294
+ ...collectInvalidCookieOptionsFacts(text, detector)
3883
4295
  ]);
3884
4296
  }
3885
4297
  function collectDuplicateArrayKeyFacts(text, detector) {
@@ -4259,6 +4671,771 @@ function collectNullsafeReturnedByReferenceFacts(text, detector) {
4259
4671
  pattern: /\b(?:static\s+)?fn\s*&\s*\([^)]*\)\s*=>\s*[^;{}\n]*\?->/gu
4260
4672
  });
4261
4673
  }
4674
+ function collectEmptyArrayLiteralSlotFacts(text, detector) {
4675
+ const kind = PHP_CORRECTNESS_FACT_KINDS.emptyArrayLiteralSlot;
4676
+ const findings = [];
4677
+ for (const literal of collectAllPhpArrayLiteralRanges(text)) {
4678
+ for (const match of findAllMatches(literal.content, /,\s*,|\[\s*,/gu)) {
4679
+ findings.push(
4680
+ createOffsetFact(text, {
4681
+ detector,
4682
+ appliesTo: "block",
4683
+ kind,
4684
+ startOffset: literal.startOffset + 1 + match.startOffset,
4685
+ endOffset: literal.startOffset + 1 + match.endOffset,
4686
+ text: match.matchedText.trim() || ","
4687
+ })
4688
+ );
4689
+ }
4690
+ }
4691
+ return dedupeFacts(findings);
4692
+ }
4693
+ function collectAllPhpArrayLiteralRanges(text) {
4694
+ const ranges = [];
4695
+ const arrayCallPattern = /\barray\s*\(/gu;
4696
+ for (const match of findAllMatches(text, arrayCallPattern)) {
4697
+ const openParen = match.endOffset - 1;
4698
+ const closeParen = findMatchingDelimiter(text, openParen, "(", ")");
4699
+ if (closeParen < 0) {
4700
+ continue;
4701
+ }
4702
+ ranges.push({
4703
+ startOffset: match.startOffset,
4704
+ endOffset: closeParen + 1,
4705
+ content: text.slice(openParen + 1, closeParen)
4706
+ });
4707
+ }
4708
+ const stack = [];
4709
+ for (let index = 0; index < text.length; index += 1) {
4710
+ const char = text[index];
4711
+ if (char === "[") {
4712
+ stack.push(index);
4713
+ continue;
4714
+ }
4715
+ if (char !== "]" || stack.length === 0) {
4716
+ continue;
4717
+ }
4718
+ const start = stack.pop();
4719
+ if (start === void 0) {
4720
+ continue;
4721
+ }
4722
+ ranges.push({
4723
+ startOffset: start,
4724
+ endOffset: index + 1,
4725
+ content: text.slice(start + 1, index)
4726
+ });
4727
+ }
4728
+ return ranges;
4729
+ }
4730
+ function collectEmptyBracketArrayAccessFacts(text, detector) {
4731
+ return collectMatchedFacts({
4732
+ text,
4733
+ detector,
4734
+ kind: PHP_CORRECTNESS_FACT_KINDS.emptyBracketArrayAccess,
4735
+ appliesTo: "block",
4736
+ pattern: /\$\w+\[\s*\](?!\s*=)/gu
4737
+ });
4738
+ }
4739
+ function collectDeprecatedUnsetCastFacts(text, detector) {
4740
+ return collectMatchedFacts({
4741
+ text,
4742
+ detector,
4743
+ kind: PHP_CORRECTNESS_FACT_KINDS.deprecatedUnsetCast,
4744
+ appliesTo: "block",
4745
+ pattern: /\(unset\)/gu
4746
+ });
4747
+ }
4748
+ function collectDuplicateDeclarationFacts(text, detector) {
4749
+ const kind = PHP_CORRECTNESS_FACT_KINDS.duplicateDeclaration;
4750
+ const findings = [];
4751
+ const seenFunctions = /* @__PURE__ */ new Map();
4752
+ const seenClasses = /* @__PURE__ */ new Map();
4753
+ for (const match of findAllMatches(
4754
+ text,
4755
+ /\bfunction\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/gu
4756
+ )) {
4757
+ const name = /function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/u.exec(
4758
+ match.matchedText
4759
+ )?.[1];
4760
+ if (!name || !isTopLevelDeclaration(text, match.startOffset)) {
4761
+ continue;
4762
+ }
4763
+ if (seenFunctions.has(name)) {
4764
+ findings.push(
4765
+ createOffsetFact(text, {
4766
+ detector,
4767
+ appliesTo: "block",
4768
+ kind,
4769
+ startOffset: match.startOffset,
4770
+ endOffset: match.endOffset,
4771
+ text: match.matchedText
4772
+ })
4773
+ );
4774
+ continue;
4775
+ }
4776
+ seenFunctions.set(name, match.startOffset);
4777
+ }
4778
+ for (const match of findAllMatches(
4779
+ text,
4780
+ /\b(?:class|interface|trait|enum)\s+([A-Za-z_][A-Za-z0-9_]*)\b/gu
4781
+ )) {
4782
+ const name = /(?:class|interface|trait|enum)\s+([A-Za-z_][A-Za-z0-9_]*)\b/u.exec(
4783
+ match.matchedText
4784
+ )?.[1];
4785
+ if (!name || !isTopLevelDeclaration(text, match.startOffset)) {
4786
+ continue;
4787
+ }
4788
+ if (seenClasses.has(name)) {
4789
+ findings.push(
4790
+ createOffsetFact(text, {
4791
+ detector,
4792
+ appliesTo: "block",
4793
+ kind,
4794
+ startOffset: match.startOffset,
4795
+ endOffset: match.endOffset,
4796
+ text: match.matchedText
4797
+ })
4798
+ );
4799
+ continue;
4800
+ }
4801
+ seenClasses.set(name, match.startOffset);
4802
+ }
4803
+ return findings;
4804
+ }
4805
+ function isTopLevelDeclaration(text, offset) {
4806
+ let braceDepth = 0;
4807
+ for (let index = 0; index < offset; index += 1) {
4808
+ const char = text[index];
4809
+ if (char === "{") {
4810
+ braceDepth += 1;
4811
+ continue;
4812
+ }
4813
+ if (char === "}") {
4814
+ braceDepth = Math.max(0, braceDepth - 1);
4815
+ }
4816
+ }
4817
+ return braceDepth === 0;
4818
+ }
4819
+ function collectNestedFunctionDeclarationFacts(text, detector) {
4820
+ const kind = PHP_CORRECTNESS_FACT_KINDS.nestedFunctionDeclaration;
4821
+ const findings = [];
4822
+ for (const match of findAllMatches(
4823
+ text,
4824
+ /\bfunction\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/gu
4825
+ )) {
4826
+ if (!isInsideFunctionBody(text, match.startOffset)) {
4827
+ continue;
4828
+ }
4829
+ findings.push(
4830
+ createOffsetFact(text, {
4831
+ detector,
4832
+ appliesTo: "block",
4833
+ kind,
4834
+ startOffset: match.startOffset,
4835
+ endOffset: match.endOffset,
4836
+ text: match.matchedText
4837
+ })
4838
+ );
4839
+ }
4840
+ return findings;
4841
+ }
4842
+ function isInsideFunctionBody(text, offset) {
4843
+ const stack = [];
4844
+ let index = 0;
4845
+ while (index < offset) {
4846
+ index = skipPhpTrivia(text, index);
4847
+ if (index >= offset) {
4848
+ break;
4849
+ }
4850
+ const remaining = text.slice(index);
4851
+ if (/^\bfunction\s+[A-Za-z_][\w]*\s*\(/u.test(remaining)) {
4852
+ const openBrace = findNextControlOpenBrace(text, index);
4853
+ if (openBrace >= 0 && openBrace < offset) {
4854
+ stack.push("function");
4855
+ index = openBrace + 1;
4856
+ continue;
4857
+ }
4858
+ }
4859
+ if (/^\b(?:class|trait|enum)\s+[A-Za-z_]/u.test(remaining)) {
4860
+ const openBrace = findNextControlOpenBrace(text, index);
4861
+ if (openBrace >= 0 && openBrace < offset) {
4862
+ stack.push("class");
4863
+ index = openBrace + 1;
4864
+ continue;
4865
+ }
4866
+ }
4867
+ const char = text[index];
4868
+ if (char === "{") {
4869
+ index += 1;
4870
+ continue;
4871
+ }
4872
+ if (char === "}") {
4873
+ stack.pop();
4874
+ index += 1;
4875
+ continue;
4876
+ }
4877
+ index += 1;
4878
+ }
4879
+ return stack[stack.length - 1] === "function";
4880
+ }
4881
+ function collectBreakContinueOutsideLoopFacts(text, detector) {
4882
+ const kind = PHP_CORRECTNESS_FACT_KINDS.breakContinueOutsideLoop;
4883
+ const findings = [];
4884
+ const controlPattern = /\b(?:break|continue)\b(?:\s+\d+\s*)?;/gu;
4885
+ for (const match of findAllMatches(text, controlPattern)) {
4886
+ const keyword = /\b(break|continue)\b/u.exec(match.matchedText)?.[1];
4887
+ if (!keyword) {
4888
+ continue;
4889
+ }
4890
+ const context = getControlFlowContext(text, match.startOffset);
4891
+ if (keyword === "continue" && context.loopDepth === 0) {
4892
+ findings.push(
4893
+ createOffsetFact(text, {
4894
+ detector,
4895
+ appliesTo: "block",
4896
+ kind,
4897
+ startOffset: match.startOffset,
4898
+ endOffset: match.endOffset,
4899
+ text: match.matchedText
4900
+ })
4901
+ );
4902
+ continue;
4903
+ }
4904
+ if (keyword === "break" && context.loopDepth === 0 && context.switchDepth === 0) {
4905
+ findings.push(
4906
+ createOffsetFact(text, {
4907
+ detector,
4908
+ appliesTo: "block",
4909
+ kind,
4910
+ startOffset: match.startOffset,
4911
+ endOffset: match.endOffset,
4912
+ text: match.matchedText
4913
+ })
4914
+ );
4915
+ }
4916
+ }
4917
+ return findings;
4918
+ }
4919
+ function getControlFlowContext(text, offset) {
4920
+ const stack = [];
4921
+ let index = 0;
4922
+ while (index < offset) {
4923
+ index = skipPhpTrivia(text, index);
4924
+ if (index >= offset) {
4925
+ break;
4926
+ }
4927
+ const remaining = text.slice(index);
4928
+ if (/^\b(?:for|while|foreach|do)\b/u.test(remaining)) {
4929
+ const openBrace = findNextControlOpenBrace(text, index);
4930
+ if (openBrace >= 0 && openBrace < offset) {
4931
+ stack.push("loop");
4932
+ index = openBrace + 1;
4933
+ continue;
4934
+ }
4935
+ }
4936
+ if (/^\bswitch\b/u.test(remaining)) {
4937
+ const openBrace = findNextControlOpenBrace(text, index);
4938
+ if (openBrace >= 0 && openBrace < offset) {
4939
+ stack.push("switch");
4940
+ index = openBrace + 1;
4941
+ continue;
4942
+ }
4943
+ }
4944
+ const char = text[index];
4945
+ if (char === "{") {
4946
+ index += 1;
4947
+ continue;
4948
+ }
4949
+ if (char === "}") {
4950
+ stack.pop();
4951
+ index += 1;
4952
+ continue;
4953
+ }
4954
+ index += 1;
4955
+ }
4956
+ let loopDepth = 0;
4957
+ let switchDepth = 0;
4958
+ for (const frame of stack) {
4959
+ if (frame === "loop") {
4960
+ loopDepth += 1;
4961
+ } else {
4962
+ switchDepth += 1;
4963
+ }
4964
+ }
4965
+ return { loopDepth, switchDepth };
4966
+ }
4967
+ function skipPhpTrivia(text, index) {
4968
+ const remaining = text.slice(index);
4969
+ if (remaining.startsWith("//") || remaining.startsWith("#")) {
4970
+ const nextLine = remaining.search(/\r?\n/u);
4971
+ return nextLine < 0 ? text.length : index + nextLine + 1;
4972
+ }
4973
+ if (remaining.startsWith("/*")) {
4974
+ const end = remaining.indexOf("*/");
4975
+ return end < 0 ? text.length : index + end + 2;
4976
+ }
4977
+ const quote = remaining[0];
4978
+ if (quote === '"' || quote === "'") {
4979
+ return skipQuotedPhpString(text, index, quote);
4980
+ }
4981
+ return index;
4982
+ }
4983
+ function skipQuotedPhpString(text, index, quote) {
4984
+ let cursor = index + 1;
4985
+ while (cursor < text.length) {
4986
+ const char = text[cursor];
4987
+ if (char === "\\") {
4988
+ cursor += 2;
4989
+ continue;
4990
+ }
4991
+ if (char === quote) {
4992
+ return cursor + 1;
4993
+ }
4994
+ cursor += 1;
4995
+ }
4996
+ return text.length;
4997
+ }
4998
+ function findNextControlOpenBrace(text, fromOffset) {
4999
+ let depth = 0;
5000
+ let index = fromOffset;
5001
+ while (index < text.length) {
5002
+ index = skipPhpTrivia(text, index);
5003
+ if (index >= text.length) {
5004
+ return -1;
5005
+ }
5006
+ const char = text[index];
5007
+ if (char === "(") {
5008
+ depth += 1;
5009
+ index += 1;
5010
+ continue;
5011
+ }
5012
+ if (char === ")") {
5013
+ depth = Math.max(0, depth - 1);
5014
+ index += 1;
5015
+ continue;
5016
+ }
5017
+ if (char === "{" && depth === 0) {
5018
+ return index;
5019
+ }
5020
+ if (char === ";" && depth === 0) {
5021
+ return -1;
5022
+ }
5023
+ index += 1;
5024
+ }
5025
+ return -1;
5026
+ }
5027
+ function collectAbstractMethodOutsideAbstractClassFacts(text, detector) {
5028
+ const kind = PHP_CORRECTNESS_FACT_KINDS.abstractMethodOutsideAbstractClass;
5029
+ const findings = [];
5030
+ const classPattern = /\b(?:(abstract)\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)\b[^{]*\{/gu;
5031
+ for (const match of findAllMatches(text, classPattern)) {
5032
+ const isAbstract = Boolean(
5033
+ /^\s*abstract\s+class\b/u.test(match.matchedText)
5034
+ );
5035
+ if (isAbstract) {
5036
+ continue;
5037
+ }
5038
+ const openBrace = match.endOffset - 1;
5039
+ const closeBrace = findMatchingDelimiter(text, openBrace, "{", "}");
5040
+ if (closeBrace < 0) {
5041
+ continue;
5042
+ }
5043
+ const body = text.slice(openBrace + 1, closeBrace);
5044
+ const abstractMethodPattern = /\babstract\s+(?:(?:public|protected|private|static)\s+)*function\s+[A-Za-z_][\w]*\s*\(/gu;
5045
+ for (const methodMatch of findAllMatches(body, abstractMethodPattern)) {
5046
+ const absoluteStart = openBrace + 1 + methodMatch.startOffset;
5047
+ const absoluteEnd = openBrace + 1 + methodMatch.endOffset;
5048
+ findings.push(
5049
+ createOffsetFact(text, {
5050
+ detector,
5051
+ appliesTo: "block",
5052
+ kind,
5053
+ startOffset: absoluteStart,
5054
+ endOffset: absoluteEnd,
5055
+ text: methodMatch.matchedText
5056
+ })
5057
+ );
5058
+ }
5059
+ }
5060
+ return findings;
5061
+ }
5062
+ function collectUselessUnsetFacts(text, detector) {
5063
+ return collectMatchedFacts({
5064
+ text,
5065
+ detector,
5066
+ kind: PHP_CORRECTNESS_FACT_KINDS.uselessUnset,
5067
+ appliesTo: "block",
5068
+ pattern: /\bunset\s*\(\s*(?:\$this->[\w]+|true|false|null|\d+|['"][^'"]*['"])\s*\)/gu
5069
+ });
5070
+ }
5071
+ function collectInvalidRegexLiteralFacts(text, detector) {
5072
+ const kind = PHP_CORRECTNESS_FACT_KINDS.invalidRegexLiteral;
5073
+ const findings = [];
5074
+ const pregPattern = /\bpreg_(?:match|match_all|replace|replace_callback|filter|grep|split)\s*\(\s*(['"])([\s\S]*?)\1/gu;
5075
+ for (const match of findAllMatches(text, pregPattern)) {
5076
+ const literalMatch = /(['"])([\s\S]*?)\1/u.exec(match.matchedText);
5077
+ if (!literalMatch) {
5078
+ continue;
5079
+ }
5080
+ const patternLiteral = literalMatch[2];
5081
+ if (!isInvalidPhpRegexPattern(patternLiteral)) {
5082
+ continue;
5083
+ }
5084
+ findings.push(
5085
+ createOffsetFact(text, {
5086
+ detector,
5087
+ appliesTo: "block",
5088
+ kind,
5089
+ startOffset: match.startOffset,
5090
+ endOffset: match.endOffset,
5091
+ text: match.matchedText
5092
+ })
5093
+ );
5094
+ }
5095
+ return findings;
5096
+ }
5097
+ function isInvalidPhpRegexPattern(literal) {
5098
+ const delimiter = literal[0];
5099
+ if (!delimiter || !/[/#~@%]/u.test(delimiter)) {
5100
+ return false;
5101
+ }
5102
+ let index = 1;
5103
+ let escaped = false;
5104
+ while (index < literal.length) {
5105
+ const char = literal[index];
5106
+ if (escaped) {
5107
+ escaped = false;
5108
+ index += 1;
5109
+ continue;
5110
+ }
5111
+ if (char === "\\") {
5112
+ escaped = true;
5113
+ index += 1;
5114
+ continue;
5115
+ }
5116
+ if (char === delimiter) {
5117
+ const body = literal.slice(1, index);
5118
+ try {
5119
+ new RegExp(body, "u");
5120
+ return false;
5121
+ } catch {
5122
+ return true;
5123
+ }
5124
+ }
5125
+ index += 1;
5126
+ }
5127
+ return true;
5128
+ }
5129
+ function collectTodoFixmeMarkerFacts(text, detector) {
5130
+ return collectMatchedFacts({
5131
+ text,
5132
+ detector,
5133
+ kind: PHP_CORRECTNESS_FACT_KINDS.todoFixmeMarker,
5134
+ appliesTo: "block",
5135
+ pattern: /(?:\/\/|#|\/\*|\*)\s*.*\b(?:TODO|FIXME|XXX)\b/giu
5136
+ });
5137
+ }
5138
+ function collectSelfAssignmentFacts(text, detector) {
5139
+ return collectMatchedFacts({
5140
+ text,
5141
+ detector,
5142
+ kind: PHP_CORRECTNESS_FACT_KINDS.selfAssignment,
5143
+ appliesTo: "block",
5144
+ pattern: /(\$this->[\w]+|\$\w+(?:->[\w]+|\[[^\]]+\])*)\s*=\s*\1\s*;/gu
5145
+ });
5146
+ }
5147
+ function collectDefaultParameterNotLastFacts(text, detector) {
5148
+ const kind = PHP_CORRECTNESS_FACT_KINDS.defaultParameterNotLast;
5149
+ const findings = [];
5150
+ const signaturePattern = /\b(?:function\s+[A-Za-z_][\w]*|fn\s*\()\s*\(([^)]*)\)/gu;
5151
+ for (const match of findAllMatches(text, signaturePattern)) {
5152
+ const paramsMatch = /\(([^)]*)\)/u.exec(match.matchedText);
5153
+ if (!paramsMatch) {
5154
+ continue;
5155
+ }
5156
+ const paramsStartInMatch = match.matchedText.indexOf("(") + 1;
5157
+ const params = splitParameterList(paramsMatch[1]);
5158
+ let sawDefault = false;
5159
+ for (const param of params) {
5160
+ const trimmed = param.text.trim();
5161
+ if (!trimmed) {
5162
+ continue;
5163
+ }
5164
+ const hasDefault = /(?:^|[^=<>!])=(?!=)/u.test(trimmed);
5165
+ if (hasDefault) {
5166
+ sawDefault = true;
5167
+ continue;
5168
+ }
5169
+ if (sawDefault) {
5170
+ const absoluteStart = match.startOffset + paramsStartInMatch + param.startOffset;
5171
+ const absoluteEnd = match.startOffset + paramsStartInMatch + param.endOffset;
5172
+ findings.push(
5173
+ createOffsetFact(text, {
5174
+ detector,
5175
+ appliesTo: "block",
5176
+ kind,
5177
+ startOffset: absoluteStart,
5178
+ endOffset: absoluteEnd,
5179
+ text: trimmed
5180
+ })
5181
+ );
5182
+ }
5183
+ }
5184
+ }
5185
+ return findings;
5186
+ }
5187
+ function splitParameterList(source) {
5188
+ const params = [];
5189
+ let start = 0;
5190
+ let depth = 0;
5191
+ for (let index = 0; index < source.length; index += 1) {
5192
+ const char = source[index];
5193
+ if (char === "(" || char === "[") {
5194
+ depth += 1;
5195
+ continue;
5196
+ }
5197
+ if (char === ")" || char === "]") {
5198
+ depth = Math.max(0, depth - 1);
5199
+ continue;
5200
+ }
5201
+ if (char === "," && depth === 0) {
5202
+ params.push({
5203
+ text: source.slice(start, index),
5204
+ startOffset: start,
5205
+ endOffset: index
5206
+ });
5207
+ start = index + 1;
5208
+ }
5209
+ }
5210
+ params.push({
5211
+ text: source.slice(start),
5212
+ startOffset: start,
5213
+ endOffset: source.length
5214
+ });
5215
+ return params;
5216
+ }
5217
+ function collectEmptyFunctionBodyFacts(text, detector) {
5218
+ return collectMatchedFacts({
5219
+ text,
5220
+ detector,
5221
+ kind: PHP_CORRECTNESS_FACT_KINDS.emptyFunctionBody,
5222
+ appliesTo: "block",
5223
+ pattern: /\bfunction\s+[A-Za-z_][\w]*\s*\([^)]*\)\s*(?::\s*[^{]+)?\{\s*(?:\/\/[^\n]*\s*|\/\*[\s\S]*?\*\/\s*)*\}/gu
5224
+ });
5225
+ }
5226
+ function collectUnknownMagicMethodFacts(text, detector) {
5227
+ const kind = PHP_CORRECTNESS_FACT_KINDS.unknownMagicMethod;
5228
+ const findings = [];
5229
+ const magicPattern = /\bfunction\s+(__[A-Za-z_][\w]*)\s*\(/gu;
5230
+ for (const match of findAllMatches(text, magicPattern)) {
5231
+ const methodName = /function\s+(__[A-Za-z_][\w]*)\s*\(/u.exec(
5232
+ match.matchedText
5233
+ )?.[1];
5234
+ if (!methodName || PHP_VALID_MAGIC_METHODS.has(methodName)) {
5235
+ continue;
5236
+ }
5237
+ findings.push(
5238
+ createOffsetFact(text, {
5239
+ detector,
5240
+ appliesTo: "block",
5241
+ kind,
5242
+ startOffset: match.startOffset,
5243
+ endOffset: match.endOffset,
5244
+ text: match.matchedText
5245
+ })
5246
+ );
5247
+ }
5248
+ return findings;
5249
+ }
5250
+ function collectCaseInsensitiveDefineFacts(text, detector) {
5251
+ return collectMatchedFacts({
5252
+ text,
5253
+ detector,
5254
+ kind: PHP_CORRECTNESS_FACT_KINDS.caseInsensitiveDefine,
5255
+ appliesTo: "block",
5256
+ pattern: /\bdefine\s*\(\s*['"][^'"]+['"]\s*,[\s\S]*?,\s*(?:true|1)\s*\)/gu
5257
+ });
5258
+ }
5259
+ function collectDeprecatedFilterConstantFacts(text, detector) {
5260
+ return collectMatchedFacts({
5261
+ text,
5262
+ detector,
5263
+ kind: PHP_CORRECTNESS_FACT_KINDS.deprecatedFilterConstant,
5264
+ appliesTo: "block",
5265
+ pattern: PHP_DEPRECATED_FILTER_CONSTANTS
5266
+ });
5267
+ }
5268
+ function collectEmptyCodeBlockFacts(text, detector) {
5269
+ return collectMatchedFacts({
5270
+ text,
5271
+ detector,
5272
+ kind: PHP_CORRECTNESS_FACT_KINDS.emptyCodeBlock,
5273
+ appliesTo: "block",
5274
+ pattern: /\b(?:if|else|elseif|try|catch|finally|for|while|foreach)\b[^{;]*\{\s*(?:\/\/[^\n]*\s*|\/\*[\s\S]*?\*\/\s*)*\}/gu
5275
+ });
5276
+ }
5277
+ function collectDeprecatedLibxmlEntityLoaderFacts(text, detector) {
5278
+ return collectMatchedFacts({
5279
+ text,
5280
+ detector,
5281
+ kind: PHP_CORRECTNESS_FACT_KINDS.deprecatedLibxmlEntityLoader,
5282
+ appliesTo: "block",
5283
+ pattern: /\blibxml_disable_entity_loader\s*\(/gu
5284
+ });
5285
+ }
5286
+ function collectRedundantStringCastConcatFacts(text, detector) {
5287
+ return collectMatchedFacts({
5288
+ text,
5289
+ detector,
5290
+ kind: PHP_CORRECTNESS_FACT_KINDS.redundantStringCastConcat,
5291
+ appliesTo: "block",
5292
+ pattern: /\(\s*string\s*\)\s*\$\w+\s*\./gu
5293
+ });
5294
+ }
5295
+ function collectMissingMemberVisibilityFacts(text, detector) {
5296
+ const kind = PHP_CORRECTNESS_FACT_KINDS.missingMemberVisibility;
5297
+ const findings = [];
5298
+ const classPattern = /\bclass\s+[A-Za-z_][\w]*\b[^{]*\{/gu;
5299
+ for (const match of findAllMatches(text, classPattern)) {
5300
+ const openBrace = match.endOffset - 1;
5301
+ const closeBrace = findMatchingDelimiter(text, openBrace, "{", "}");
5302
+ if (closeBrace < 0) {
5303
+ continue;
5304
+ }
5305
+ const body = text.slice(openBrace + 1, closeBrace);
5306
+ const memberPattern = /^\s*(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/\s*)*(?:function\s+[A-Za-z_]|(?:var\s+)?\$[A-Za-z_])/gmu;
5307
+ for (const memberMatch of body.matchAll(memberPattern)) {
5308
+ const line = memberMatch[0];
5309
+ const trimmed = line.trimStart();
5310
+ if (/^(?:public|private|protected)\b/u.test(trimmed)) {
5311
+ continue;
5312
+ }
5313
+ if (/^function\s+__/u.test(trimmed)) {
5314
+ continue;
5315
+ }
5316
+ const matchIndex = memberMatch.index ?? 0;
5317
+ const absoluteStart = openBrace + 1 + matchIndex;
5318
+ const absoluteEnd = absoluteStart + line.trimEnd().length;
5319
+ findings.push(
5320
+ createOffsetFact(text, {
5321
+ detector,
5322
+ appliesTo: "block",
5323
+ kind,
5324
+ startOffset: absoluteStart,
5325
+ endOffset: absoluteEnd,
5326
+ text: line.trimEnd()
5327
+ })
5328
+ );
5329
+ }
5330
+ }
5331
+ return findings;
5332
+ }
5333
+ function collectFunctionComparisonFacts(text, detector) {
5334
+ return collectMatchedFacts({
5335
+ text,
5336
+ detector,
5337
+ kind: PHP_CORRECTNESS_FACT_KINDS.functionComparison,
5338
+ appliesTo: "block",
5339
+ pattern: /\$\w+\s*(?:==|===|!=|!==)\s*['"][^'"]+['"]|\[\s*\$\w+\s*,\s*['"][^'"]+['"]\s*\]\s*(?:<|>|<=|>=)\s*\[\s*\$\w+\s*,\s*['"][^'"]+['"]\s*\]/gu
5340
+ });
5341
+ }
5342
+ function collectUselessPostIncrementFacts(text, detector) {
5343
+ return collectMatchedFacts({
5344
+ text,
5345
+ detector,
5346
+ kind: PHP_CORRECTNESS_FACT_KINDS.uselessPostIncrement,
5347
+ appliesTo: "block",
5348
+ pattern: /^\s*(?:\$\w+(?:\[[^\]]+\])?(?:->\w+)*|\$this->\w+)\s*\+\+\s*;/gmu
5349
+ });
5350
+ }
5351
+ function collectNestedSwitchFacts(text, detector) {
5352
+ const kind = PHP_CORRECTNESS_FACT_KINDS.nestedSwitch;
5353
+ const findings = [];
5354
+ const switchPattern = /\bswitch\s*\(/gu;
5355
+ for (const switchMatch of findAllMatches(text, switchPattern)) {
5356
+ const openBrace = findSwitchOpenBrace(text, switchMatch.endOffset);
5357
+ if (openBrace < 0) {
5358
+ continue;
5359
+ }
5360
+ const parentDepth = countEnclosingSwitchDepth(text, switchMatch.startOffset);
5361
+ if (parentDepth === 0) {
5362
+ continue;
5363
+ }
5364
+ findings.push(
5365
+ createOffsetFact(text, {
5366
+ detector,
5367
+ appliesTo: "block",
5368
+ kind,
5369
+ startOffset: switchMatch.startOffset,
5370
+ endOffset: switchMatch.endOffset,
5371
+ text: switchMatch.matchedText
5372
+ })
5373
+ );
5374
+ }
5375
+ return findings;
5376
+ }
5377
+ function countEnclosingSwitchDepth(text, offset) {
5378
+ let depth = 0;
5379
+ let index = 0;
5380
+ while (index < offset) {
5381
+ index = skipPhpTrivia(text, index);
5382
+ if (index >= offset) {
5383
+ break;
5384
+ }
5385
+ const remaining = text.slice(index);
5386
+ if (/^\bswitch\b/u.test(remaining)) {
5387
+ const openBrace = findNextControlOpenBrace(text, index);
5388
+ if (openBrace >= 0 && openBrace < offset) {
5389
+ depth += 1;
5390
+ index = openBrace + 1;
5391
+ continue;
5392
+ }
5393
+ }
5394
+ const char = text[index];
5395
+ if (char === "{") {
5396
+ index += 1;
5397
+ continue;
5398
+ }
5399
+ if (char === "}") {
5400
+ depth = Math.max(0, depth - 1);
5401
+ index += 1;
5402
+ continue;
5403
+ }
5404
+ index += 1;
5405
+ }
5406
+ return depth;
5407
+ }
5408
+ function collectInvalidCookieOptionsFacts(text, detector) {
5409
+ const kind = PHP_CORRECTNESS_FACT_KINDS.invalidCookieOptions;
5410
+ const findings = [];
5411
+ const cookiePattern = /\bset(?:raw)?cookie\s*\(/gu;
5412
+ for (const match of findAllMatches(text, cookiePattern)) {
5413
+ const openParen = match.endOffset - 1;
5414
+ const closeParen = findMatchingDelimiter(text, openParen, "(", ")");
5415
+ if (closeParen < 0) {
5416
+ continue;
5417
+ }
5418
+ const callText = text.slice(match.startOffset, closeParen + 1);
5419
+ const optionsPattern = /['"]([\w-]+)['"]\s*=>/gu;
5420
+ for (const optionMatch of findAllMatches(callText, optionsPattern)) {
5421
+ const key = /['"]([\w-]+)['"]\s*=>/u.exec(optionMatch.matchedText)?.[1];
5422
+ if (!key || PHP_VALID_COOKIE_OPTION_KEYS.has(key.toLowerCase())) {
5423
+ continue;
5424
+ }
5425
+ findings.push(
5426
+ createOffsetFact(text, {
5427
+ detector,
5428
+ appliesTo: "block",
5429
+ kind,
5430
+ startOffset: match.startOffset + optionMatch.startOffset,
5431
+ endOffset: match.startOffset + optionMatch.endOffset,
5432
+ text: optionMatch.matchedText
5433
+ })
5434
+ );
5435
+ }
5436
+ }
5437
+ return findings;
5438
+ }
4262
5439
 
4263
5440
  // libs/adapters/shared/src/lib/polyglot/domains/php-baseline-security.ts
4264
5441
  var PHP_BASELINE_SECURITY_FACT_KINDS = {
@@ -4267,7 +5444,9 @@ var PHP_BASELINE_SECURITY_FACT_KINDS = {
4267
5444
  weakCipher: "php.security.weak-cipher",
4268
5445
  insecureSessionIdGeneration: "php.security.insecure-session-id-generation",
4269
5446
  xmlExternalEntity: "php.security.xml-external-entity",
4270
- debugFunctionExposure: "php.security.debug-function-exposure"
5447
+ debugFunctionExposure: "php.security.debug-function-exposure",
5448
+ unsafeNewStatic: "php.security.unsafe-new-static",
5449
+ deprecatedLibxmlEntityLoader: "php.security.deprecated-libxml-entity-loader"
4271
5450
  };
4272
5451
  var WEAK_OPENSSL_CIPHER_PATTERN = /\b(?:DES|RC4|BF|ECB)\b|(?:^|[^A-Za-z])DES(?:[^A-Za-z]|$)/iu;
4273
5452
  var XML_LOAD_PATTERN = /\b(?:simplexml_load_(?:file|string|xml)|domxml_(?:open_mem|xml)|xml_parse)\s*\(|\b(?:DOMDocument|SimpleXMLElement)\b[\s\S]{0,120}?->\s*load(?:XML|HTML)?\s*\(/g;
@@ -4292,7 +5471,9 @@ function collectPhpBaselineSecurityFacts(options) {
4292
5471
  matchesTainted
4293
5472
  }),
4294
5473
  ...collectXmlExternalEntityFacts(text, detector),
4295
- ...collectDebugFunctionExposureFacts(text, path, detector)
5474
+ ...collectDebugFunctionExposureFacts(text, path, detector),
5475
+ ...collectUnsafeNewStaticFacts(text, detector),
5476
+ ...collectDeprecatedLibxmlEntityLoaderFacts2(text, detector)
4296
5477
  ]);
4297
5478
  }
4298
5479
  function collectNoDynamicEvalFacts(text, detector) {
@@ -4441,6 +5622,51 @@ function collectDebugFunctionExposureFacts(text, path, detector) {
4441
5622
  pattern: /\b(?:var_dump|print_r|debug_zval_dump)\s*\(|\bxdebug_[a-z_]+\s*\(/gi
4442
5623
  });
4443
5624
  }
5625
+ function collectUnsafeNewStaticFacts(text, detector) {
5626
+ const kind = PHP_BASELINE_SECURITY_FACT_KINDS.unsafeNewStatic;
5627
+ const findings = [];
5628
+ const classPattern = /\b(?:(?:abstract|readonly|final)\s+)*class\s+([A-Za-z_][\w]*)\b[^{]*\{/gu;
5629
+ for (const match of findAllMatches(text, classPattern)) {
5630
+ const declarationWindow = text.slice(
5631
+ Math.max(0, match.startOffset - 12),
5632
+ match.endOffset
5633
+ );
5634
+ if (/\bfinal\b/u.test(declarationWindow)) {
5635
+ continue;
5636
+ }
5637
+ const openBrace = match.endOffset - 1;
5638
+ const closeBrace = findMatchingDelimiter(text, openBrace, "{", "}");
5639
+ if (closeBrace < 0) {
5640
+ continue;
5641
+ }
5642
+ const body = text.slice(openBrace + 1, closeBrace);
5643
+ const staticPattern = /\bnew\s+static\s*\(/gu;
5644
+ for (const staticMatch of findAllMatches(body, staticPattern)) {
5645
+ const absoluteStart = openBrace + 1 + staticMatch.startOffset;
5646
+ const absoluteEnd = openBrace + 1 + staticMatch.endOffset;
5647
+ findings.push(
5648
+ createOffsetFact(text, {
5649
+ detector,
5650
+ appliesTo: "block",
5651
+ kind,
5652
+ startOffset: absoluteStart,
5653
+ endOffset: absoluteEnd,
5654
+ text: staticMatch.matchedText
5655
+ })
5656
+ );
5657
+ }
5658
+ }
5659
+ return findings;
5660
+ }
5661
+ function collectDeprecatedLibxmlEntityLoaderFacts2(text, detector) {
5662
+ return collectMatchedFacts({
5663
+ text,
5664
+ detector,
5665
+ kind: PHP_BASELINE_SECURITY_FACT_KINDS.deprecatedLibxmlEntityLoader,
5666
+ appliesTo: "block",
5667
+ pattern: /\blibxml_disable_entity_loader\s*\(/gu
5668
+ });
5669
+ }
4444
5670
 
4445
5671
  // libs/adapters/shared/src/lib/polyglot/domains/java-open-redirect.ts
4446
5672
  var javaRedirectSinkPattern = /\bsendRedirect\s*\(|\bnew\s+RedirectView\s*\(/g;
@@ -4551,6 +5777,49 @@ function collectSpringConfigDebugExposureFacts(options) {
4551
5777
  return [...debugPairs, ...verboseLogging];
4552
5778
  }
4553
5779
 
5780
+ // libs/adapters/shared/src/lib/polyglot/domains/android-screenshot-exposure.ts
5781
+ var defaultAndroidActivityPattern = /\bclass\s+[A-Za-z_][A-Za-z0-9_]*\s+extends\s+(?:[A-Za-z_][A-Za-z0-9_]*Activity|Activity|AppCompatActivity|ComponentActivity|FragmentActivity)\b/g;
5782
+ var secureFlagEnabledPattern = /\b(?:getWindow\(\)\.)?(?:addFlags|setFlags)\s*\([^;\n]*FLAG_SECURE\b/g;
5783
+ var secureFlagClearedPattern = /\b(?:getWindow\(\)\.)?clearFlags\s*\([^;\n]*FLAG_SECURE\b/g;
5784
+ var sensitiveAndroidScreenPattern = /\b(?:account|auth|balance|billing|card|credential|login|otp|password|payment|pin|secret|session|token|transfer|wallet)\b|[A-Za-z0-9_]*(?:Otp|Passcode|Password|Pin|Secret|Session|Token)\b/i;
5785
+ function collectAndroidScreenshotExposureFacts(options) {
5786
+ const hasSecureFlag = secureFlagEnabledPattern.test(options.text);
5787
+ const hasClearedSecureFlag = secureFlagClearedPattern.test(options.text);
5788
+ if (hasSecureFlag && !hasClearedSecureFlag) {
5789
+ return [];
5790
+ }
5791
+ if (!hasClearedSecureFlag && !sensitiveAndroidScreenPattern.test(options.text)) {
5792
+ return [];
5793
+ }
5794
+ return collectMatchedFacts({
5795
+ text: options.text,
5796
+ detector: options.detector,
5797
+ kind: "security.android-screenshot-exposure",
5798
+ pattern: options.activityPattern ?? defaultAndroidActivityPattern,
5799
+ appliesTo: options.appliesTo ?? "file",
5800
+ props: () => ({
5801
+ reason: hasClearedSecureFlag ? "flag-secure-cleared" : "flag-secure-missing"
5802
+ }),
5803
+ textValue: ({ matchedText }) => matchedText.trim()
5804
+ });
5805
+ }
5806
+
5807
+ // libs/adapters/shared/src/lib/polyglot/domains/android-world-readable-mode.ts
5808
+ var defaultAndroidWorldReadablePattern = /\b(?:Context\.)?MODE_WORLD_(?:READABLE|WRITABLE|WRITEABLE)\b/g;
5809
+ function collectAndroidWorldReadableModeFacts(options) {
5810
+ return collectMatchedFacts({
5811
+ text: options.text,
5812
+ detector: options.detector,
5813
+ kind: "security.android-world-readable-mode",
5814
+ pattern: options.pattern ?? defaultAndroidWorldReadablePattern,
5815
+ appliesTo: options.appliesTo ?? "block",
5816
+ props: ({ matchedText }) => ({
5817
+ mode: matchedText.trim()
5818
+ }),
5819
+ textValue: ({ matchedText }) => matchedText.trim()
5820
+ });
5821
+ }
5822
+
4554
5823
  // libs/adapters/shared/src/lib/polyglot/domains/java-framework-security.ts
4555
5824
  var JAVA_FRAMEWORK_SECURITY_FACT_KINDS = {
4556
5825
  springPermitAllDefault: "java.security.spring-permit-all-default",
@@ -8269,6 +9538,12 @@ function collectRustQualityMaintainabilityFacts(options) {
8269
9538
  }
8270
9539
 
8271
9540
  // libs/adapters/shared/src/lib/polyglot/domains/performance.ts
9541
+ var PHP_PERFORMANCE_FACT_KINDS = {
9542
+ noRegexConstructionInLoop: "php.performance.no-regex-construction-in-loop",
9543
+ noSyncFsInRequestPath: "php.performance.no-sync-fs-in-request-path",
9544
+ expensiveLoopCondition: "php.performance.expensive-loop-condition",
9545
+ noUnboundedConcurrency: "php.performance.no-unbounded-concurrency"
9546
+ };
8272
9547
  function collectSharedPerformanceFacts(options, languagePrefix) {
8273
9548
  const { text, detector } = options;
8274
9549
  return [
@@ -8302,7 +9577,57 @@ function collectJavaPerformanceFacts(options) {
8302
9577
  return collectSharedPerformanceFacts(options, "java");
8303
9578
  }
8304
9579
  function collectPhpPerformanceFacts(options) {
8305
- return collectSharedPerformanceFacts(options, "php");
9580
+ const { text, detector } = options;
9581
+ return [
9582
+ ...collectMatchedFacts({
9583
+ text,
9584
+ detector,
9585
+ kind: PHP_PERFORMANCE_FACT_KINDS.noRegexConstructionInLoop,
9586
+ pattern: /\b(?:for|while)\b[\s\S]{0,300}\bpreg_(?:match|match_all|replace|replace_callback|filter|grep|split)\s*\(/gu,
9587
+ appliesTo: "block"
9588
+ }),
9589
+ ...collectPhpSyncFsInRequestPathFacts(options),
9590
+ ...collectMatchedFacts({
9591
+ text,
9592
+ detector,
9593
+ kind: PHP_PERFORMANCE_FACT_KINDS.expensiveLoopCondition,
9594
+ pattern: /\b(?:for|while)\s*\([\s\S]{0,240}?\b(?:count|sizeof|strlen|preg_match|preg_match_all|array_sum|in_array|file_get_contents|file_exists|glob)\s*\(/gu,
9595
+ appliesTo: "block"
9596
+ }),
9597
+ ...collectMatchedFacts({
9598
+ text,
9599
+ detector,
9600
+ kind: PHP_PERFORMANCE_FACT_KINDS.noUnboundedConcurrency,
9601
+ appliesTo: "block",
9602
+ pattern: /\b(?:GuzzleHttp\\Promise\\(?:all|unwrap)|Amp\\Promise\\all)\s*\(\s*\$\w+/gu
9603
+ })
9604
+ ];
9605
+ }
9606
+ var phpSyncFsCallPattern = /\b(?:file_get_contents|fopen|readfile|file|scandir|glob)\s*\(/gu;
9607
+ function collectPhpSyncFsInRequestPathFacts(options) {
9608
+ const { text, detector, state, matchesTainted } = options;
9609
+ if (!state || !matchesTainted) {
9610
+ return [];
9611
+ }
9612
+ return collectSnippetFacts({
9613
+ text,
9614
+ detector,
9615
+ kind: PHP_PERFORMANCE_FACT_KINDS.noSyncFsInRequestPath,
9616
+ pattern: phpSyncFsCallPattern,
9617
+ state,
9618
+ appliesTo: "block",
9619
+ predicate: (snippet, scanState) => isPhpSyncFsInRequestHandler(text, snippet.startOffset) && matchesTainted(snippet.text, scanState)
9620
+ });
9621
+ }
9622
+ function isPhpSyncFsInRequestHandler(text, callStartOffset) {
9623
+ const prefix = text.slice(0, callStartOffset);
9624
+ const functionStart = prefix.lastIndexOf("function");
9625
+ if (functionStart < 0) {
9626
+ return false;
9627
+ }
9628
+ return /\$_(?:GET|POST|REQUEST|COOKIE|FILES|SERVER)\b/u.test(
9629
+ text.slice(functionStart, callStartOffset)
9630
+ );
8306
9631
  }
8307
9632
  function collectPythonPerformanceFacts(options) {
8308
9633
  return collectSharedPerformanceFacts(options, "py");
@@ -8314,6 +9639,323 @@ function collectRustPerformanceFacts(options) {
8314
9639
  return collectSharedPerformanceFacts(options, "rust");
8315
9640
  }
8316
9641
 
9642
+ // libs/adapters/cloudformation/src/lib/cloudformation.ts
9643
+ var import_node_fs4 = require("node:fs");
9644
+ var import_node_os = require("node:os");
9645
+ var import_node_path5 = require("node:path");
9646
+
9647
+ // libs/adapters/cloudformation/src/lib/collect-cfn-lint-facts.util.ts
9648
+ var CFN_LINT_FACT_KIND = "cfn.lint.finding";
9649
+ var CFN_LINT_DETECTOR = "cfn-lint";
9650
+ function lineColumnToOffset(text, line, column) {
9651
+ const lines = text.split("\n");
9652
+ const safeLine = Math.max(line, 1);
9653
+ let offset = 0;
9654
+ for (let index = 0; index < safeLine - 1 && index < lines.length; index += 1) {
9655
+ offset += lines[index].length + 1;
9656
+ }
9657
+ const lineText = lines[safeLine - 1] ?? "";
9658
+ offset += Math.min(Math.max(column - 1, 0), lineText.length);
9659
+ return offset;
9660
+ }
9661
+ function createRangeFromLineColumns(text, startLine, startColumn, endLine, endColumn) {
9662
+ const startOffset = lineColumnToOffset(text, startLine, startColumn);
9663
+ const endOffset = Math.max(
9664
+ startOffset + 1,
9665
+ lineColumnToOffset(text, endLine, endColumn)
9666
+ );
9667
+ const lines = text.split("\n");
9668
+ const excerptLines = lines.slice(
9669
+ Math.max(startLine - 1, 0),
9670
+ Math.min(endLine, lines.length)
9671
+ );
9672
+ return {
9673
+ startOffset,
9674
+ endOffset,
9675
+ excerpt: excerptLines.join("\n"),
9676
+ range: {
9677
+ startLine,
9678
+ startColumn,
9679
+ endLine,
9680
+ endColumn
9681
+ }
9682
+ };
9683
+ }
9684
+ function collectCfnLintFacts(text, findings) {
9685
+ return findings.map((finding) => {
9686
+ const positioned = createRangeFromLineColumns(
9687
+ text,
9688
+ finding.line,
9689
+ finding.column,
9690
+ finding.endLine,
9691
+ finding.endColumn
9692
+ );
9693
+ return {
9694
+ id: [
9695
+ CFN_LINT_DETECTOR,
9696
+ CFN_LINT_FACT_KIND,
9697
+ finding.ruleId,
9698
+ positioned.range.startLine,
9699
+ positioned.range.startColumn,
9700
+ positioned.range.endLine,
9701
+ positioned.range.endColumn
9702
+ ].join(":"),
9703
+ kind: CFN_LINT_FACT_KIND,
9704
+ appliesTo: "file",
9705
+ range: positioned.range,
9706
+ text: positioned.excerpt,
9707
+ props: {
9708
+ ruleId: finding.ruleId,
9709
+ level: finding.level,
9710
+ message: finding.message,
9711
+ line: finding.line,
9712
+ column: finding.column
9713
+ }
9714
+ };
9715
+ });
9716
+ }
9717
+
9718
+ // libs/adapters/cloudformation/src/lib/is-cloudformation-template.util.ts
9719
+ var import_node_path4 = require("node:path");
9720
+ function looksLikeCloudFormationPath2(filePath) {
9721
+ const normalized = filePath.replace(/\\/gu, "/").toLowerCase();
9722
+ const fileName = normalized.split("/").pop() ?? normalized;
9723
+ if (/(?:^|\/)(?:templates?|cloudformation|cfn|sam|infra|iac)(?:\/|$)/u.test(
9724
+ normalized
9725
+ )) {
9726
+ return true;
9727
+ }
9728
+ return /(?:template|cloudformation|stack|cfn|sam)[^/]*\.(?:ya?ml|json)$/u.test(
9729
+ fileName
9730
+ );
9731
+ }
9732
+ function hasServerlessTransform(transform) {
9733
+ if (transform === "AWS::Serverless") {
9734
+ return true;
9735
+ }
9736
+ if (Array.isArray(transform)) {
9737
+ return transform.includes("AWS::Serverless");
9738
+ }
9739
+ return false;
9740
+ }
9741
+ function hasCloudFormationObjectMarkers(value) {
9742
+ if (typeof value["AWSTemplateFormatVersion"] === "string") {
9743
+ return true;
9744
+ }
9745
+ if (value["Resources"] !== void 0 && typeof value["Resources"] === "object" && value["Resources"] !== null) {
9746
+ return true;
9747
+ }
9748
+ return hasServerlessTransform(value["Transform"]);
9749
+ }
9750
+ function isCloudFormationTemplate(path, text) {
9751
+ const trimmed = text.trim();
9752
+ if (!trimmed) {
9753
+ return false;
9754
+ }
9755
+ const extension = (0, import_node_path4.extname)(path).toLowerCase();
9756
+ if (extension === ".json") {
9757
+ try {
9758
+ const parsed = JSON.parse(trimmed);
9759
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9760
+ return false;
9761
+ }
9762
+ return hasCloudFormationObjectMarkers(parsed);
9763
+ } catch {
9764
+ return false;
9765
+ }
9766
+ }
9767
+ if (/AWSTemplateFormatVersion\s*:/m.test(trimmed)) {
9768
+ return true;
9769
+ }
9770
+ if (/^Resources\s*:/m.test(trimmed)) {
9771
+ return true;
9772
+ }
9773
+ if (/Transform\s*:\s*(?:AWS::Serverless|['"]AWS::Serverless['"])/m.test(trimmed)) {
9774
+ return true;
9775
+ }
9776
+ return false;
9777
+ }
9778
+
9779
+ // libs/adapters/cloudformation/src/lib/parse-cfn-lint-json.util.ts
9780
+ function isCfnLintJsonMatch(value) {
9781
+ return typeof value === "object" && value !== null;
9782
+ }
9783
+ function parseCfnLintJson(stdout) {
9784
+ const trimmed = stdout.trim();
9785
+ if (!trimmed) {
9786
+ return [];
9787
+ }
9788
+ let parsed;
9789
+ try {
9790
+ parsed = JSON.parse(trimmed);
9791
+ } catch {
9792
+ return [];
9793
+ }
9794
+ if (!Array.isArray(parsed)) {
9795
+ return [];
9796
+ }
9797
+ const findings = [];
9798
+ for (const entry of parsed) {
9799
+ if (!isCfnLintJsonMatch(entry)) {
9800
+ continue;
9801
+ }
9802
+ const ruleId = entry.Rule?.Id?.trim();
9803
+ if (!ruleId) {
9804
+ continue;
9805
+ }
9806
+ const line = entry.Location?.Start?.LineNumber ?? 1;
9807
+ const column = entry.Location?.Start?.ColumnNumber ?? 1;
9808
+ const endLine = entry.Location?.End?.LineNumber ?? line;
9809
+ const endColumn = entry.Location?.End?.ColumnNumber ?? column;
9810
+ const message = entry.Message?.trim() ?? "";
9811
+ const level = entry.Level?.trim() ?? "Unknown";
9812
+ findings.push({
9813
+ ruleId,
9814
+ level,
9815
+ message,
9816
+ line,
9817
+ column,
9818
+ endLine,
9819
+ endColumn
9820
+ });
9821
+ }
9822
+ return findings;
9823
+ }
9824
+
9825
+ // libs/adapters/cloudformation/src/lib/run-cfn-lint.util.ts
9826
+ var import_node_child_process = require("node:child_process");
9827
+ function readExecOutput(value) {
9828
+ if (typeof value === "string") {
9829
+ return value;
9830
+ }
9831
+ if (Buffer.isBuffer(value)) {
9832
+ return value.toString("utf8");
9833
+ }
9834
+ return "";
9835
+ }
9836
+ function runCfnLint(filePath) {
9837
+ try {
9838
+ const stdout = (0, import_node_child_process.execFileSync)("cfn-lint", ["-f", "json", filePath], {
9839
+ encoding: "utf8",
9840
+ maxBuffer: 16 * 1024 * 1024
9841
+ });
9842
+ return {
9843
+ ok: true,
9844
+ stdout,
9845
+ stderr: "",
9846
+ exitCode: 0
9847
+ };
9848
+ } catch (error) {
9849
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
9850
+ return {
9851
+ ok: false,
9852
+ stdout: "",
9853
+ stderr: "",
9854
+ exitCode: -1,
9855
+ errorCode: "ENOENT"
9856
+ };
9857
+ }
9858
+ if (typeof error === "object" && error !== null && "stdout" in error) {
9859
+ return {
9860
+ ok: true,
9861
+ stdout: readExecOutput(error.stdout),
9862
+ stderr: readExecOutput(
9863
+ "stderr" in error ? error.stderr : void 0
9864
+ ),
9865
+ exitCode: "status" in error && typeof error.status === "number" ? error.status : 1
9866
+ };
9867
+ }
9868
+ const message = error instanceof Error ? error.message : "Unexpected cfn-lint failure.";
9869
+ return {
9870
+ ok: false,
9871
+ stdout: "",
9872
+ stderr: message,
9873
+ exitCode: 1
9874
+ };
9875
+ }
9876
+ }
9877
+
9878
+ // libs/adapters/cloudformation/src/lib/cloudformation.ts
9879
+ var SUPPORTED_EXTENSIONS = [".yaml", ".yml", ".json"];
9880
+ function templateExtensionForPath(path) {
9881
+ const extension = (0, import_node_path5.extname)(path).toLowerCase();
9882
+ if (extension === ".yaml" || extension === ".yml" || extension === ".json") {
9883
+ return extension;
9884
+ }
9885
+ return ".yaml";
9886
+ }
9887
+ function createMissingCfnLintDiagnostic(path) {
9888
+ return createDiagnostic({
9889
+ code: "adapter.cloudformation.cfn-lint-missing",
9890
+ message: "The `cfn-lint` executable was not found on PATH. Install cfn-lint to analyze CloudFormation templates.",
9891
+ severity: "warning",
9892
+ details: {
9893
+ path
9894
+ }
9895
+ });
9896
+ }
9897
+ function createCfnLintFailureDiagnostic(path, stderr) {
9898
+ return createDiagnostic({
9899
+ code: "adapter.cloudformation.cfn-lint-failed",
9900
+ message: `cfn-lint failed while analyzing \`${path}\`.`,
9901
+ severity: "warning",
9902
+ details: {
9903
+ path,
9904
+ stderr
9905
+ }
9906
+ });
9907
+ }
9908
+ function analyzeCloudFormationFile(path, text, options = {}) {
9909
+ if (!isCloudFormationTemplate(path, text)) {
9910
+ return {
9911
+ success: true,
9912
+ data: buildAnalyzedFileWithFacts(path, "cloudformation", text, [])
9913
+ };
9914
+ }
9915
+ const runner = options.runCfnLint ?? runCfnLint;
9916
+ const tempDirectory = (0, import_node_fs4.mkdtempSync)((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "critiq-cfn-"));
9917
+ const tempPath = (0, import_node_path5.join)(
9918
+ tempDirectory,
9919
+ `${(0, import_node_path5.basename)(path, (0, import_node_path5.extname)(path)) || "template"}${templateExtensionForPath(path)}`
9920
+ );
9921
+ try {
9922
+ (0, import_node_fs4.writeFileSync)(tempPath, text, "utf8");
9923
+ const lintResult = runner(tempPath);
9924
+ if (lintResult.errorCode === "ENOENT") {
9925
+ return {
9926
+ success: true,
9927
+ data: buildAnalyzedFileWithFacts(path, "cloudformation", text, []),
9928
+ diagnostics: [createMissingCfnLintDiagnostic(path)]
9929
+ };
9930
+ }
9931
+ if (!lintResult.ok && !lintResult.stdout.trim()) {
9932
+ return {
9933
+ success: true,
9934
+ data: buildAnalyzedFileWithFacts(path, "cloudformation", text, []),
9935
+ diagnostics: [
9936
+ createCfnLintFailureDiagnostic(path, lintResult.stderr)
9937
+ ]
9938
+ };
9939
+ }
9940
+ const findings = parseCfnLintJson(lintResult.stdout);
9941
+ const facts = collectCfnLintFacts(text, findings);
9942
+ return {
9943
+ success: true,
9944
+ data: buildAnalyzedFileWithFacts(path, "cloudformation", text, facts)
9945
+ };
9946
+ } finally {
9947
+ (0, import_node_fs4.rmSync)(tempDirectory, { recursive: true, force: true });
9948
+ }
9949
+ }
9950
+ var cloudformationSourceAdapter = {
9951
+ packageName: "@critiq/adapter-cloudformation",
9952
+ supportedExtensions: SUPPORTED_EXTENSIONS,
9953
+ supportedLanguages: ["cloudformation"],
9954
+ canHandlePath: looksLikeCloudFormationPath2,
9955
+ canHandle: isCloudFormationTemplate,
9956
+ analyze: analyzeCloudFormationFile
9957
+ };
9958
+
8317
9959
  // libs/adapters/go/src/lib/go.ts
8318
9960
  var hardcodedCredentialPattern = /(?:^|\n)\s*(?:const|var)?\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?::=|=)\s*["'`][^"'`\n]{8,}["'`]/g;
8319
9961
  var fileReadCallPattern = /\b(?:os|ioutil)\.ReadFile\s*\(/g;
@@ -8625,6 +10267,14 @@ var javaAdapterDefinition = {
8625
10267
  text,
8626
10268
  detector
8627
10269
  }),
10270
+ ...collectAndroidScreenshotExposureFacts({
10271
+ text,
10272
+ detector
10273
+ }),
10274
+ ...collectAndroidWorldReadableModeFacts({
10275
+ text,
10276
+ detector
10277
+ }),
8628
10278
  ...collectSpringConfigDebugExposureFacts({
8629
10279
  text,
8630
10280
  detector,
@@ -8860,7 +10510,13 @@ var phpAdapterDefinition = {
8860
10510
  }),
8861
10511
  ...collectPhpTestingHygieneFacts({ text, path, detector }),
8862
10512
  ...collectPhpQualityMaintainabilityFacts({ text, path, detector }),
8863
- ...collectPhpPerformanceFacts({ text, path, detector }),
10513
+ ...collectPhpPerformanceFacts({
10514
+ text,
10515
+ path,
10516
+ detector,
10517
+ state,
10518
+ matchesTainted: matchesPhpTainted
10519
+ }),
8864
10520
  ...collectPhpCorrectnessFacts({ text, detector })
8865
10521
  ]
8866
10522
  };
@@ -9236,6 +10892,8 @@ var rubyAdapterDefinition = {
9236
10892
  detector,
9237
10893
  pattern: weakHashCallPattern5
9238
10894
  }),
10895
+ ...collectRubyGeneralSecurityFacts({ text, path, detector }),
10896
+ ...collectRubyBugRiskFacts({ text, detector }),
9239
10897
  ...collectRubyRailsSecurityFacts({
9240
10898
  text,
9241
10899
  detector,
@@ -9503,7 +11161,7 @@ function looksLikeRustSqlInterpolation(expression) {
9503
11161
 
9504
11162
  // libs/adapters/typescript/src/lib/typescript.ts
9505
11163
  var import_typescript_estree = require("@typescript-eslint/typescript-estree");
9506
- var import_node_path4 = require("node:path");
11164
+ var import_node_path6 = require("node:path");
9507
11165
 
9508
11166
  // libs/adapters/typescript/src/lib/ast/source-text.ts
9509
11167
  function isNodeLike(value) {
@@ -17927,6 +19585,323 @@ var collectAdditionalPublicSecurityFacts = (context) => {
17927
19585
  ];
17928
19586
  };
17929
19587
 
19588
+ // libs/adapters/typescript/src/lib/custom-facts/additional-public-security/electron-shell-open-external-unvalidated.ts
19589
+ var openExternalCalleePattern = /(^|\.)(openExternal)$/u;
19590
+ var requestDrivenUrlPattern = /\b(?:req|request|body|query|params)\b/u;
19591
+ function collectElectronShellOpenExternalUnvalidatedFacts(context) {
19592
+ const facts = [];
19593
+ walkAst(context.program, (node) => {
19594
+ if (node.type !== "CallExpression") {
19595
+ return;
19596
+ }
19597
+ const calleeText2 = getCalleeText(node.callee, context.sourceText);
19598
+ if (!calleeText2 || !openExternalCalleePattern.test(calleeText2)) {
19599
+ return;
19600
+ }
19601
+ const urlText = getNodeText(
19602
+ node.arguments[0],
19603
+ context.sourceText
19604
+ );
19605
+ if (!urlText || !requestDrivenUrlPattern.test(urlText)) {
19606
+ return;
19607
+ }
19608
+ facts.push(
19609
+ createObservedFact({
19610
+ appliesTo: "block",
19611
+ kind: FACT_KINDS.electronShellOpenExternalUnvalidated,
19612
+ node,
19613
+ nodeIds: context.nodeIds,
19614
+ text: calleeText2,
19615
+ props: {
19616
+ url: urlText
19617
+ }
19618
+ })
19619
+ );
19620
+ });
19621
+ return facts;
19622
+ }
19623
+
19624
+ // libs/adapters/typescript/src/lib/custom-facts/substrate/client-security.ts
19625
+ var electronDangerousWebPreferences = [
19626
+ {
19627
+ name: "allowRunningInsecureContent",
19628
+ insecureBooleanValue: true
19629
+ },
19630
+ {
19631
+ name: "contextIsolation",
19632
+ insecureBooleanValue: false
19633
+ },
19634
+ {
19635
+ name: "enableRemoteModule",
19636
+ insecureBooleanValue: true
19637
+ },
19638
+ {
19639
+ name: "nodeIntegration",
19640
+ insecureBooleanValue: true
19641
+ },
19642
+ {
19643
+ name: "sandbox",
19644
+ insecureBooleanValue: false
19645
+ },
19646
+ {
19647
+ name: "webSecurity",
19648
+ insecureBooleanValue: false
19649
+ }
19650
+ ];
19651
+ var electronTrustedOriginValidatorNames = /* @__PURE__ */ new Set([
19652
+ "allowlistedOrigin",
19653
+ "assertAllowedOrigin",
19654
+ "assertTrustedSenderFrame",
19655
+ "assertTrustedWebContents",
19656
+ "ensureAllowedOrigin",
19657
+ "ensureTrustedSender",
19658
+ "validateAllowedOrigin",
19659
+ "validateTrustedOrigin",
19660
+ "validateTrustedSender"
19661
+ ]);
19662
+ var electronPrivilegedIpcSinkPattern = /\b(?:BrowserWindow|dialog\.(?:showOpenDialog|showSaveDialog)|exec|fs\.(?:appendFile|appendFileSync|readFile|readFileSync|rm|rmSync|unlink|unlinkSync|writeFile|writeFileSync)|openExternal|process\.env|shell\.(?:openExternal|openPath|showItemInFolder)|spawn|systemPreferences|webContents)\b/u;
19663
+ function isElectronTrustedOriginValidatorName(calleeText2) {
19664
+ return Boolean(
19665
+ calleeText2 && electronTrustedOriginValidatorNames.has(calleeText2)
19666
+ );
19667
+ }
19668
+ function isElectronSensitiveStorageKey(text) {
19669
+ return isAuthStorageKey(text) || isAuthLikeText(text);
19670
+ }
19671
+ function isLikelyPrivilegedElectronIpcBody(bodyText2) {
19672
+ return Boolean(
19673
+ bodyText2 && electronPrivilegedIpcSinkPattern.test(bodyText2)
19674
+ );
19675
+ }
19676
+
19677
+ // libs/adapters/typescript/src/lib/custom-facts/client-application-security.ts
19678
+ var ELECTRON_DANGEROUS_WEBPREFERENCES_FACT_KIND = "security.electron-dangerous-webpreferences";
19679
+ var ELECTRON_MISSING_IPC_ORIGIN_CHECK_FACT_KIND = "security.electron-missing-ipc-origin-check";
19680
+ var ELECTRON_INSECURE_LOCAL_STATE_FACT_KIND = "security.electron-insecure-local-state";
19681
+ var browserWindowCalleePattern = /(^|\.)(BrowserWindow)$/u;
19682
+ var ipcMainHandlerPattern = /(^|\.)(ipcMain)\.(handle|on)$/u;
19683
+ function collectElectronStoreConstructorNames(context) {
19684
+ const constructorNames = /* @__PURE__ */ new Set();
19685
+ walkAst(context.program, (node) => {
19686
+ if (node.type === "ImportDeclaration" && node.source.value === "electron-store") {
19687
+ for (const specifier of node.specifiers) {
19688
+ constructorNames.add(specifier.local.name);
19689
+ }
19690
+ return;
19691
+ }
19692
+ if (node.type !== "VariableDeclarator" || node.id.type !== "Identifier" || !node.init || node.init.type !== "CallExpression" || node.init.callee.type !== "Identifier" || node.init.callee.name !== "require") {
19693
+ return;
19694
+ }
19695
+ const sourceValue = getNodeText(
19696
+ node.init.arguments[0],
19697
+ context.sourceText
19698
+ )?.replace(/^['"]|['"]$/gu, "");
19699
+ if (sourceValue === "electron-store") {
19700
+ constructorNames.add(node.id.name);
19701
+ }
19702
+ });
19703
+ return constructorNames;
19704
+ }
19705
+ function collectElectronStoreInstanceNames(context, constructorNames) {
19706
+ const instanceNames = /* @__PURE__ */ new Set();
19707
+ if (constructorNames.size === 0) {
19708
+ return instanceNames;
19709
+ }
19710
+ walkAst(context.program, (node) => {
19711
+ if (node.type !== "VariableDeclarator" || node.id.type !== "Identifier" || !node.init || node.init.type !== "NewExpression") {
19712
+ return;
19713
+ }
19714
+ const calleeText2 = getNodeText(node.init.callee, context.sourceText);
19715
+ if (calleeText2 && constructorNames.has(calleeText2)) {
19716
+ instanceNames.add(node.id.name);
19717
+ }
19718
+ });
19719
+ return instanceNames;
19720
+ }
19721
+ function hasElectronOriginCheck(handler, sourceText) {
19722
+ const firstParam = handler.params[0];
19723
+ if (!firstParam || firstParam.type !== "Identifier") {
19724
+ return false;
19725
+ }
19726
+ const originExpressions = [
19727
+ `${firstParam.name}.senderFrame.origin`,
19728
+ `${firstParam.name}.senderFrame.url`,
19729
+ `${firstParam.name}.sender.getURL()`
19730
+ ];
19731
+ let checked = false;
19732
+ walkAst(handler.body, (node) => {
19733
+ if (checked) {
19734
+ return;
19735
+ }
19736
+ if (node.type === "IfStatement" || node.type === "ConditionalExpression") {
19737
+ const testText = getNodeText(node.test, sourceText)?.replace(/\s+/gu, " ");
19738
+ if (testText && originExpressions.some((expression) => testText.includes(expression))) {
19739
+ checked = true;
19740
+ }
19741
+ return;
19742
+ }
19743
+ if (node.type === "SwitchStatement") {
19744
+ const discriminantText = getNodeText(
19745
+ node.discriminant,
19746
+ sourceText
19747
+ )?.replace(/\s+/gu, " ");
19748
+ if (discriminantText && originExpressions.some(
19749
+ (expression) => discriminantText.includes(expression)
19750
+ )) {
19751
+ checked = true;
19752
+ }
19753
+ return;
19754
+ }
19755
+ if (node.type !== "CallExpression") {
19756
+ return;
19757
+ }
19758
+ const calleeText2 = getCalleeText(node.callee, sourceText);
19759
+ const firstArgumentText = getNodeText(
19760
+ node.arguments[0],
19761
+ sourceText
19762
+ )?.replace(/\s+/gu, " ");
19763
+ if (isElectronTrustedOriginValidatorName(calleeText2) && firstArgumentText && originExpressions.some((expression) => firstArgumentText.includes(expression))) {
19764
+ checked = true;
19765
+ }
19766
+ });
19767
+ return checked;
19768
+ }
19769
+ function collectElectronDangerousWebPreferenceFacts(context) {
19770
+ const objectBindings = collectObjectBindings(context);
19771
+ const facts = [];
19772
+ walkAst(context.program, (node) => {
19773
+ if (node.type !== "NewExpression") {
19774
+ return;
19775
+ }
19776
+ const calleeText2 = getNodeText(node.callee, context.sourceText);
19777
+ if (!calleeText2 || !browserWindowCalleePattern.test(calleeText2)) {
19778
+ return;
19779
+ }
19780
+ const windowOptions = resolveObjectExpression(node.arguments[0], objectBindings);
19781
+ const webPreferences = resolveObjectExpression(
19782
+ getObjectProperty(windowOptions, "webPreferences")?.value,
19783
+ objectBindings
19784
+ );
19785
+ if (!webPreferences) {
19786
+ return;
19787
+ }
19788
+ for (const preference of electronDangerousWebPreferences) {
19789
+ const property = getObjectProperty(webPreferences, preference.name);
19790
+ if (!property || !isBooleanLiteral(
19791
+ property.value,
19792
+ preference.insecureBooleanValue
19793
+ )) {
19794
+ continue;
19795
+ }
19796
+ facts.push(
19797
+ createObservedFact({
19798
+ appliesTo: "block",
19799
+ kind: ELECTRON_DANGEROUS_WEBPREFERENCES_FACT_KIND,
19800
+ node: property,
19801
+ nodeIds: context.nodeIds,
19802
+ props: {
19803
+ preference: preference.name,
19804
+ configuredValue: getNodeText(
19805
+ property.value,
19806
+ context.sourceText
19807
+ ),
19808
+ sink: calleeText2
19809
+ },
19810
+ text: `webPreferences.${preference.name}`
19811
+ })
19812
+ );
19813
+ }
19814
+ });
19815
+ return facts;
19816
+ }
19817
+ function collectElectronIpcOriginFacts(context) {
19818
+ const functionBindings = resolveFunctionBindings(context);
19819
+ const facts = [];
19820
+ walkAst(context.program, (node) => {
19821
+ if (node.type !== "CallExpression") {
19822
+ return;
19823
+ }
19824
+ const calleeText2 = getCalleeText(node.callee, context.sourceText);
19825
+ if (!calleeText2 || !ipcMainHandlerPattern.test(calleeText2)) {
19826
+ return;
19827
+ }
19828
+ const handler = resolveFunctionLike(node.arguments[1], functionBindings);
19829
+ const bodyText2 = handler ? getNodeText(handler.body, context.sourceText) : void 0;
19830
+ if (!handler || !isLikelyPrivilegedElectronIpcBody(bodyText2) || hasElectronOriginCheck(handler, context.sourceText)) {
19831
+ return;
19832
+ }
19833
+ facts.push(
19834
+ createObservedFact({
19835
+ appliesTo: "block",
19836
+ kind: ELECTRON_MISSING_IPC_ORIGIN_CHECK_FACT_KIND,
19837
+ node,
19838
+ nodeIds: context.nodeIds,
19839
+ props: {
19840
+ channel: getNodeText(
19841
+ node.arguments[0],
19842
+ context.sourceText
19843
+ ),
19844
+ sink: calleeText2
19845
+ },
19846
+ text: `${calleeText2}(${getNodeText(
19847
+ node.arguments[0],
19848
+ context.sourceText
19849
+ ) ?? ""})`
19850
+ })
19851
+ );
19852
+ });
19853
+ return facts;
19854
+ }
19855
+ function collectElectronInsecureLocalStateFacts(context) {
19856
+ const constructorNames = collectElectronStoreConstructorNames(context);
19857
+ const instanceNames = collectElectronStoreInstanceNames(
19858
+ context,
19859
+ constructorNames
19860
+ );
19861
+ const facts = [];
19862
+ if (instanceNames.size === 0) {
19863
+ return facts;
19864
+ }
19865
+ walkAst(context.program, (node) => {
19866
+ if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") {
19867
+ return;
19868
+ }
19869
+ if (node.callee.object.type !== "Identifier" || !instanceNames.has(node.callee.object.name) || getMemberPropertyName(node.callee) !== "set") {
19870
+ return;
19871
+ }
19872
+ const storageKeyText = getNodeText(
19873
+ node.arguments[0],
19874
+ context.sourceText
19875
+ );
19876
+ const storageValueText = getNodeText(
19877
+ node.arguments[1],
19878
+ context.sourceText
19879
+ );
19880
+ if (!isElectronSensitiveStorageKey(storageKeyText) && !isElectronSensitiveStorageKey(storageValueText)) {
19881
+ return;
19882
+ }
19883
+ facts.push(
19884
+ createObservedFact({
19885
+ appliesTo: "block",
19886
+ kind: ELECTRON_INSECURE_LOCAL_STATE_FACT_KIND,
19887
+ node,
19888
+ nodeIds: context.nodeIds,
19889
+ props: {
19890
+ key: storageKeyText,
19891
+ sink: `${node.callee.object.name}.set`
19892
+ },
19893
+ text: `${node.callee.object.name}.set(${storageKeyText ?? "unknown"})`
19894
+ })
19895
+ );
19896
+ });
19897
+ return facts;
19898
+ }
19899
+ var collectClientApplicationSecurityFacts = (context) => [
19900
+ ...collectElectronDangerousWebPreferenceFacts(context),
19901
+ ...collectElectronIpcOriginFacts(context),
19902
+ ...collectElectronInsecureLocalStateFacts(context)
19903
+ ];
19904
+
17930
19905
  // libs/adapters/typescript/src/lib/custom-facts/typescript-async-correctness.ts
17931
19906
  var ARRAY_METHODS_EXPECTING_SYNC_CALLBACK = /* @__PURE__ */ new Set([
17932
19907
  "every",
@@ -25380,6 +27355,8 @@ var collectWeakCryptoFacts = (context) => {
25380
27355
  function collectAdditionalTypeScriptFacts(context) {
25381
27356
  const facts = [
25382
27357
  ...collectAdditionalPublicSecurityFacts(context),
27358
+ ...collectClientApplicationSecurityFacts(context),
27359
+ ...collectElectronShellOpenExternalUnvalidatedFacts(context),
25383
27360
  ...collectTypescriptAsyncCorrectnessFacts(context),
25384
27361
  ...collectTypescriptCoreLanguageCorrectnessFacts(context),
25385
27362
  ...collectTypescriptCorrectnessLanguageExtendedFacts(context),
@@ -25562,7 +27539,7 @@ var typescriptSourceAdapter = {
25562
27539
  analyze: analyzeTypeScriptFile
25563
27540
  };
25564
27541
  function extensionToLanguage(path) {
25565
- switch ((0, import_node_path4.extname)(path).toLowerCase()) {
27542
+ switch ((0, import_node_path6.extname)(path).toLowerCase()) {
25566
27543
  case ".js":
25567
27544
  case ".jsx":
25568
27545
  return "javascript";
@@ -25573,7 +27550,7 @@ function extensionToLanguage(path) {
25573
27550
  }
25574
27551
  }
25575
27552
  function supportsJsx(path) {
25576
- return [".jsx", ".tsx"].includes((0, import_node_path4.extname)(path).toLowerCase());
27553
+ return [".jsx", ".tsx"].includes((0, import_node_path6.extname)(path).toLowerCase());
25577
27554
  }
25578
27555
  function analyzeTypeScriptFile(path, text) {
25579
27556
  try {
@@ -25629,7 +27606,7 @@ function analyzeTypeScriptFile(path, text) {
25629
27606
  }
25630
27607
 
25631
27608
  // libs/runtime/check-runner/src/lib/check-runner/registry.ts
25632
- var import_node_path7 = require("node:path");
27609
+ var import_node_path9 = require("node:path");
25633
27610
 
25634
27611
  // libs/core/ir/src/lib/ir.ts
25635
27612
  var import_node_crypto2 = require("node:crypto");
@@ -25885,8 +27862,8 @@ function normalizeRuleDocument(validatedRuleDocument) {
25885
27862
  }
25886
27863
 
25887
27864
  // libs/core/rules-dsl/src/lib/rules-dsl.ts
25888
- var import_node_fs4 = require("node:fs");
25889
- var import_node_path5 = require("node:path");
27865
+ var import_node_fs5 = require("node:fs");
27866
+ var import_node_path7 = require("node:path");
25890
27867
  var import_zod6 = require("zod");
25891
27868
 
25892
27869
  // libs/core/rules-dsl/src/lib/rules-dsl-schema.ts
@@ -25906,6 +27883,7 @@ var ruleLanguageSchema = import_zod5.z.enum([
25906
27883
  "ruby",
25907
27884
  "rust",
25908
27885
  "dockerfile",
27886
+ "cloudformation",
25909
27887
  "all"
25910
27888
  ]);
25911
27889
  var ruleStabilitySchema = import_zod5.z.enum(["stable", "experimental"]);
@@ -26149,16 +28127,16 @@ var ruleDocumentV0Alpha1Schema = import_zod5.z.object({
26149
28127
  // libs/core/rules-dsl/src/lib/rules-dsl.ts
26150
28128
  function loadRuleDocumentV0Alpha1JsonSchema() {
26151
28129
  const candidatePaths = [
26152
- (0, import_node_path5.resolve)(__dirname, "./schema/rule-document-v0alpha1.schema.json"),
26153
- (0, import_node_path5.resolve)(__dirname, "../../schema/rule-document-v0alpha1.schema.json"),
26154
- (0, import_node_path5.resolve)(
28130
+ (0, import_node_path7.resolve)(__dirname, "./schema/rule-document-v0alpha1.schema.json"),
28131
+ (0, import_node_path7.resolve)(__dirname, "../../schema/rule-document-v0alpha1.schema.json"),
28132
+ (0, import_node_path7.resolve)(
26155
28133
  __dirname,
26156
28134
  "../../../../../workspace_modules/@critiq/core-rules-dsl/schema/rule-document-v0alpha1.schema.json"
26157
28135
  )
26158
28136
  ];
26159
28137
  for (const candidatePath of candidatePaths) {
26160
- if ((0, import_node_fs4.existsSync)(candidatePath)) {
26161
- return JSON.parse((0, import_node_fs4.readFileSync)(candidatePath, "utf8"));
28138
+ if ((0, import_node_fs5.existsSync)(candidatePath)) {
28139
+ return JSON.parse((0, import_node_fs5.readFileSync)(candidatePath, "utf8"));
26162
28140
  }
26163
28141
  }
26164
28142
  throw new Error("Unable to locate rule-document-v0alpha1.schema.json.");
@@ -26210,7 +28188,7 @@ function summarizeValidatedRuleDocument(validatedRuleDocument) {
26210
28188
  }
26211
28189
 
26212
28190
  // libs/core/rules-dsl/src/lib/rules-dsl-loader.ts
26213
- var import_node_fs5 = require("node:fs");
28191
+ var import_node_fs6 = require("node:fs");
26214
28192
  var import_node_url3 = require("node:url");
26215
28193
  function toSourceMap(sourceMap) {
26216
28194
  return Object.fromEntries(
@@ -26272,7 +28250,7 @@ function loadRuleText(text, uri) {
26272
28250
  }
26273
28251
  function loadRuleFile(path) {
26274
28252
  try {
26275
- const text = (0, import_node_fs5.readFileSync)(path, "utf8");
28253
+ const text = (0, import_node_fs6.readFileSync)(path, "utf8");
26276
28254
  const uri = (0, import_node_url3.pathToFileURL)(path).href;
26277
28255
  return loadRuleText(text, uri);
26278
28256
  } catch (error) {
@@ -27006,10 +28984,10 @@ function validateRuleDocumentSemantics(validatedRuleDocument) {
27006
28984
  function validateLoadedRuleDocument(loadedRuleDocument) {
27007
28985
  const contractValidation = validateLoadedRuleDocumentContract(loadedRuleDocument);
27008
28986
  if (!contractValidation.success) {
27009
- const failure = contractValidation;
28987
+ const failure2 = contractValidation;
27010
28988
  return {
27011
28989
  success: false,
27012
- diagnostics: failure.diagnostics
28990
+ diagnostics: failure2.diagnostics
27013
28991
  };
27014
28992
  }
27015
28993
  const semanticValidation = validateRuleDocumentSemantics(contractValidation.data);
@@ -27027,16 +29005,16 @@ function validateLoadedRuleDocument(loadedRuleDocument) {
27027
29005
  }
27028
29006
 
27029
29007
  // libs/runtime/check-runner/src/lib/check-runner/shared.ts
27030
- var import_node_fs6 = require("node:fs");
27031
- var import_node_path6 = require("node:path");
29008
+ var import_node_fs7 = require("node:fs");
29009
+ var import_node_path8 = require("node:path");
27032
29010
  var DEFAULT_CATALOG_PACKAGE_NAME = "@critiq/rules";
27033
29011
  var RULE_CATALOG_FILENAME = "catalog.yaml";
27034
29012
  function toPosixPath(value) {
27035
- return value.split(import_node_path6.sep).join("/");
29013
+ return value.split(import_node_path8.sep).join("/");
27036
29014
  }
27037
29015
  function toDisplayPath(cwd, absolutePath) {
27038
- const relativePath = toPosixPath((0, import_node_path6.relative)(cwd, absolutePath));
27039
- if (relativePath.length > 0 && !relativePath.startsWith("..") && !(0, import_node_path6.isAbsolute)(relativePath)) {
29016
+ const relativePath = toPosixPath((0, import_node_path8.relative)(cwd, absolutePath));
29017
+ if (relativePath.length > 0 && !relativePath.startsWith("..") && !(0, import_node_path8.isAbsolute)(relativePath)) {
27040
29018
  return relativePath;
27041
29019
  }
27042
29020
  return toPosixPath(absolutePath);
@@ -27061,7 +29039,7 @@ function isSkippableDirectory(currentDirectory, name) {
27061
29039
  )) {
27062
29040
  return true;
27063
29041
  }
27064
- return name === "cache" && currentDirectory.split(import_node_path6.sep).at(-1) === ".yarn";
29042
+ return name === "cache" && currentDirectory.split(import_node_path8.sep).at(-1) === ".yarn";
27065
29043
  }
27066
29044
  function walkFiles(rootDirectory) {
27067
29045
  const files = [];
@@ -27071,11 +29049,11 @@ function walkFiles(rootDirectory) {
27071
29049
  if (!currentDirectory) {
27072
29050
  continue;
27073
29051
  }
27074
- const entries = (0, import_node_fs6.readdirSync)(currentDirectory, { withFileTypes: true }).sort(
29052
+ const entries = (0, import_node_fs7.readdirSync)(currentDirectory, { withFileTypes: true }).sort(
27075
29053
  (left, right) => left.name.localeCompare(right.name)
27076
29054
  );
27077
29055
  for (const entry of entries) {
27078
- const absolutePath = (0, import_node_path6.resolve)(currentDirectory, entry.name);
29056
+ const absolutePath = (0, import_node_path8.resolve)(currentDirectory, entry.name);
27079
29057
  if (entry.isDirectory()) {
27080
29058
  if (!isSkippableDirectory(currentDirectory, entry.name)) {
27081
29059
  queue.push(absolutePath);
@@ -27100,7 +29078,7 @@ function readTextFileSafe(path) {
27100
29078
  try {
27101
29079
  return {
27102
29080
  success: true,
27103
- text: (0, import_node_fs6.readFileSync)(path, "utf8")
29081
+ text: (0, import_node_fs7.readFileSync)(path, "utf8")
27104
29082
  };
27105
29083
  } catch (error) {
27106
29084
  return {
@@ -27125,16 +29103,16 @@ function loadNormalizedRulesForCatalog(absoluteRulePaths) {
27125
29103
  )) {
27126
29104
  const loadResult = loadRuleFile(absolutePath);
27127
29105
  if (!loadResult.success) {
27128
- const failure = loadResult;
27129
- diagnostics.push(...failure.diagnostics);
29106
+ const failure2 = loadResult;
29107
+ diagnostics.push(...failure2.diagnostics);
27130
29108
  continue;
27131
29109
  }
27132
29110
  const contractValidation = validateLoadedRuleDocumentContract(
27133
29111
  loadResult.data
27134
29112
  );
27135
29113
  if (!contractValidation.success) {
27136
- const failure = contractValidation;
27137
- diagnostics.push(...failure.diagnostics);
29114
+ const failure2 = contractValidation;
29115
+ diagnostics.push(...failure2.diagnostics);
27138
29116
  continue;
27139
29117
  }
27140
29118
  const semanticValidation = validateRuleDocumentSemantics(
@@ -27173,13 +29151,13 @@ function defaultCatalogPackageRootsFromEnvironment() {
27173
29151
  if (!rulesRoot) {
27174
29152
  return {};
27175
29153
  }
27176
- const directRoot = (0, import_node_path6.resolve)(rulesRoot);
29154
+ const directRoot = (0, import_node_path8.resolve)(rulesRoot);
27177
29155
  const candidateRoots = [
27178
29156
  directRoot,
27179
- (0, import_node_path6.resolve)(directRoot, "libs/rules/catalog")
29157
+ (0, import_node_path8.resolve)(directRoot, "libs/rules/catalog")
27180
29158
  ];
27181
29159
  for (const candidateRoot of candidateRoots) {
27182
- if ((0, import_node_fs6.existsSync)((0, import_node_path6.resolve)(candidateRoot, RULE_CATALOG_FILENAME))) {
29160
+ if ((0, import_node_fs7.existsSync)((0, import_node_path8.resolve)(candidateRoot, RULE_CATALOG_FILENAME))) {
27183
29161
  return {
27184
29162
  [DEFAULT_CATALOG_PACKAGE_NAME]: candidateRoot
27185
29163
  };
@@ -27197,7 +29175,7 @@ function resolveCatalogPackageForRuntime(_displayRoot, packageName, options) {
27197
29175
  packageRootOverrides,
27198
29176
  hasOverrideCatalog(packageRootOverride) {
27199
29177
  return Boolean(
27200
- packageRootOverride && (0, import_node_fs6.existsSync)((0, import_node_path6.resolve)(packageRootOverride, RULE_CATALOG_FILENAME))
29178
+ packageRootOverride && (0, import_node_fs7.existsSync)((0, import_node_path8.resolve)(packageRootOverride, RULE_CATALOG_FILENAME))
27201
29179
  );
27202
29180
  }
27203
29181
  };
@@ -27220,11 +29198,33 @@ function createSourceAdapterRegistry(adapters) {
27220
29198
  }));
27221
29199
  return {
27222
29200
  adapters: normalizedAdapters,
27223
- findAdapterForPath(path) {
27224
- const extension = normalizeExtension((0, import_node_path7.extname)(path));
27225
- return normalizedAdapters.find(
29201
+ findAdapterForPath(path, text) {
29202
+ const extension = normalizeExtension((0, import_node_path9.extname)(path));
29203
+ const candidates = normalizedAdapters.filter(
27226
29204
  (adapter) => adapter.supportedExtensions.includes(extension)
27227
29205
  );
29206
+ if (candidates.length === 0) {
29207
+ return void 0;
29208
+ }
29209
+ if (candidates.length === 1) {
29210
+ const adapter = candidates[0];
29211
+ if (adapter.canHandle) {
29212
+ if (text === void 0) {
29213
+ return adapter.canHandlePath?.(path) ? adapter : void 0;
29214
+ }
29215
+ return adapter.canHandle(path, text) ? adapter : void 0;
29216
+ }
29217
+ return adapter;
29218
+ }
29219
+ if (text !== void 0) {
29220
+ for (const adapter of candidates) {
29221
+ if (adapter.canHandle?.(path, text)) {
29222
+ return adapter;
29223
+ }
29224
+ }
29225
+ return candidates.find((adapter) => !adapter.canHandle) ?? candidates[0];
29226
+ }
29227
+ return candidates.find((adapter) => !adapter.canHandle) ?? candidates[0];
27228
29228
  },
27229
29229
  hasAdapterForLanguage(language) {
27230
29230
  return normalizedAdapters.some(
@@ -27249,6 +29249,7 @@ function createSourceAdapterRegistry(adapters) {
27249
29249
  }
27250
29250
  function createDefaultSourceAdapterRegistry() {
27251
29251
  return createSourceAdapterRegistry([
29252
+ cloudformationSourceAdapter,
27252
29253
  goSourceAdapter,
27253
29254
  javaSourceAdapter,
27254
29255
  phpSourceAdapter,
@@ -27261,12 +29262,12 @@ function createDefaultSourceAdapterRegistry() {
27261
29262
 
27262
29263
  // libs/runtime/check-runner/src/lib/check-runner/scope.ts
27263
29264
  var import_minimatch2 = require("minimatch");
27264
- var import_node_child_process = require("node:child_process");
27265
- var import_node_fs7 = require("node:fs");
27266
- var import_node_path9 = require("node:path");
29265
+ var import_node_child_process2 = require("node:child_process");
29266
+ var import_node_fs8 = require("node:fs");
29267
+ var import_node_path11 = require("node:path");
27267
29268
 
27268
29269
  // libs/runtime/check-runner/src/lib/secrets-scanner/eligibility.ts
27269
- var import_node_path8 = require("node:path");
29270
+ var import_node_path10 = require("node:path");
27270
29271
  var SECRETS_SCAN_MAX_FILE_BYTES = 512 * 1024;
27271
29272
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
27272
29273
  ".png",
@@ -27371,7 +29372,7 @@ var NO_EXTENSION_NAMES = /* @__PURE__ */ new Set([
27371
29372
  ]);
27372
29373
  function isSecretsEligiblePath(displayPath) {
27373
29374
  const lower = displayPath.toLowerCase();
27374
- const ext = (0, import_node_path8.extname)(lower);
29375
+ const ext = (0, import_node_path10.extname)(lower);
27375
29376
  const base = lower.includes("/") ? lower.slice(lower.lastIndexOf("/") + 1) : lower;
27376
29377
  if (BINARY_EXTENSIONS.has(ext)) {
27377
29378
  return false;
@@ -27437,13 +29438,13 @@ var defaultIgnoredPathPatterns = [
27437
29438
  "**/*_generated.go"
27438
29439
  ];
27439
29440
  function isPathWithinDirectory(directoryPath, candidatePath) {
27440
- const relativePath = (0, import_node_path9.relative)(directoryPath, candidatePath);
27441
- return relativePath.length === 0 || !relativePath.startsWith("..") && !(0, import_node_path9.isAbsolute)(relativePath);
29441
+ const relativePath = (0, import_node_path11.relative)(directoryPath, candidatePath);
29442
+ return relativePath.length === 0 || !relativePath.startsWith("..") && !(0, import_node_path11.isAbsolute)(relativePath);
27442
29443
  }
27443
29444
  function tryResolveGitRepoRoot(workingDirectory) {
27444
29445
  try {
27445
29446
  return toPosixPath(
27446
- (0, import_node_child_process.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
29447
+ (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
27447
29448
  cwd: workingDirectory,
27448
29449
  encoding: "utf8",
27449
29450
  stdio: ["ignore", "pipe", "pipe"]
@@ -27465,10 +29466,10 @@ function resolveCheckTarget(cwd, target) {
27465
29466
  ]
27466
29467
  };
27467
29468
  }
27468
- const candidatePath = (0, import_node_path9.resolve)(cwd, resolvedTarget);
29469
+ const candidatePath = (0, import_node_path11.resolve)(cwd, resolvedTarget);
27469
29470
  try {
27470
- const absolutePath = toPosixPath((0, import_node_fs7.realpathSync)(candidatePath));
27471
- const stats = (0, import_node_fs7.statSync)(absolutePath);
29471
+ const absolutePath = toPosixPath((0, import_node_fs8.realpathSync)(candidatePath));
29472
+ const stats = (0, import_node_fs8.statSync)(absolutePath);
27472
29473
  if (!stats.isDirectory() && !stats.isFile()) {
27473
29474
  return {
27474
29475
  success: false,
@@ -27479,7 +29480,7 @@ function resolveCheckTarget(cwd, target) {
27479
29480
  ]
27480
29481
  };
27481
29482
  }
27482
- const scopeDirectory = stats.isDirectory() ? absolutePath : (0, import_node_path9.dirname)(absolutePath);
29483
+ const scopeDirectory = stats.isDirectory() ? absolutePath : (0, import_node_path11.dirname)(absolutePath);
27483
29484
  const repoRoot = tryResolveGitRepoRoot(scopeDirectory);
27484
29485
  return {
27485
29486
  success: true,
@@ -27526,10 +29527,10 @@ function normalizeGitDiffPath(value) {
27526
29527
  }
27527
29528
  return value;
27528
29529
  }
27529
- function parseGitDiffChangedRanges(output) {
29530
+ function parseGitDiffChangedRanges(output2) {
27530
29531
  const changedRanges = /* @__PURE__ */ new Map();
27531
29532
  let currentPath = null;
27532
- for (const line of output.split(/\r?\n/)) {
29533
+ for (const line of output2.split(/\r?\n/)) {
27533
29534
  if (line.startsWith("+++ ")) {
27534
29535
  currentPath = normalizeGitDiffPath(line.slice(4));
27535
29536
  if (currentPath && !changedRanges.has(currentPath)) {
@@ -27559,7 +29560,7 @@ function runGitCommand(cwd, args) {
27559
29560
  try {
27560
29561
  return {
27561
29562
  success: true,
27562
- stdout: (0, import_node_child_process.execFileSync)("git", args, {
29563
+ stdout: (0, import_node_child_process2.execFileSync)("git", args, {
27563
29564
  cwd,
27564
29565
  encoding: "utf8",
27565
29566
  stdio: ["ignore", "pipe", "pipe"]
@@ -27617,8 +29618,8 @@ function resolveCheckScope(target, baseRef, headRef, registry) {
27617
29618
  return diffScope;
27618
29619
  }
27619
29620
  function readGitStagedFileText(repoRoot, absolutePath) {
27620
- const relPath = toPosixPath((0, import_node_path9.relative)(repoRoot, absolutePath));
27621
- if (!relPath || relPath.startsWith("..") || (0, import_node_path9.isAbsolute)(relPath)) {
29621
+ const relPath = toPosixPath((0, import_node_path11.relative)(repoRoot, absolutePath));
29622
+ if (!relPath || relPath.startsWith("..") || (0, import_node_path11.isAbsolute)(relPath)) {
27622
29623
  return {
27623
29624
  success: false,
27624
29625
  diagnostics: [
@@ -27634,7 +29635,7 @@ function readGitStagedFileText(repoRoot, absolutePath) {
27634
29635
  };
27635
29636
  }
27636
29637
  try {
27637
- const text = (0, import_node_child_process.execFileSync)("git", ["show", `:${relPath}`], {
29638
+ const text = (0, import_node_child_process2.execFileSync)("git", ["show", `:${relPath}`], {
27638
29639
  cwd: repoRoot,
27639
29640
  encoding: "utf8",
27640
29641
  stdio: ["ignore", "pipe", "pipe"],
@@ -27714,18 +29715,18 @@ function resolveStagedSecretsScope(target) {
27714
29715
  };
27715
29716
  }
27716
29717
  const changedRangesByRelativePath = parseGitDiffChangedRanges(diffResult.stdout);
27717
- const files = changedFilesResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => (0, import_node_path9.resolve)(target.repoRoot, line)).filter(
27718
- (absolutePath) => target.isDirectory ? isPathWithinDirectory(target.absolutePath, absolutePath) : (0, import_node_path9.resolve)(absolutePath) === target.absolutePath
29718
+ const files = changedFilesResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => (0, import_node_path11.resolve)(target.repoRoot, line)).filter(
29719
+ (absolutePath) => target.isDirectory ? isPathWithinDirectory(target.absolutePath, absolutePath) : (0, import_node_path11.resolve)(absolutePath) === target.absolutePath
27719
29720
  ).filter((absolutePath) => {
27720
29721
  try {
27721
- return (0, import_node_fs7.statSync)(absolutePath).isFile();
29722
+ return (0, import_node_fs8.statSync)(absolutePath).isFile();
27722
29723
  } catch {
27723
29724
  return true;
27724
29725
  }
27725
29726
  }).sort((left, right) => left.localeCompare(right));
27726
29727
  const changedRangesByAbsolutePath = /* @__PURE__ */ new Map();
27727
29728
  for (const absolutePath of files) {
27728
- const relativePath = toPosixPath((0, import_node_path9.relative)(target.repoRoot, absolutePath));
29729
+ const relativePath = toPosixPath((0, import_node_path11.relative)(target.repoRoot, absolutePath));
27729
29730
  changedRangesByAbsolutePath.set(
27730
29731
  absolutePath,
27731
29732
  changedRangesByRelativePath.get(relativePath) ?? []
@@ -27835,18 +29836,18 @@ function resolveRepositoryDiffScope(target, baseRef, headRef, includeAbsolutePat
27835
29836
  };
27836
29837
  }
27837
29838
  const changedRangesByRelativePath = parseGitDiffChangedRanges(diffResult.stdout);
27838
- const files = changedFilesResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => (0, import_node_path9.resolve)(target.repoRoot, line)).filter(
27839
- (absolutePath) => includeAbsolutePath(absolutePath) && (target.isDirectory ? isPathWithinDirectory(target.absolutePath, absolutePath) : (0, import_node_path9.resolve)(absolutePath) === target.absolutePath)
29839
+ const files = changedFilesResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => (0, import_node_path11.resolve)(target.repoRoot, line)).filter(
29840
+ (absolutePath) => includeAbsolutePath(absolutePath) && (target.isDirectory ? isPathWithinDirectory(target.absolutePath, absolutePath) : (0, import_node_path11.resolve)(absolutePath) === target.absolutePath)
27840
29841
  ).filter((absolutePath) => {
27841
29842
  try {
27842
- return (0, import_node_fs7.statSync)(absolutePath).isFile();
29843
+ return (0, import_node_fs8.statSync)(absolutePath).isFile();
27843
29844
  } catch {
27844
29845
  return false;
27845
29846
  }
27846
29847
  }).sort((left, right) => left.localeCompare(right));
27847
29848
  const changedRangesByAbsolutePath = /* @__PURE__ */ new Map();
27848
29849
  for (const absolutePath of files) {
27849
- const relativePath = toPosixPath((0, import_node_path9.relative)(target.repoRoot, absolutePath));
29850
+ const relativePath = toPosixPath((0, import_node_path11.relative)(target.repoRoot, absolutePath));
27850
29851
  changedRangesByAbsolutePath.set(
27851
29852
  absolutePath,
27852
29853
  changedRangesByRelativePath.get(relativePath) ?? []
@@ -28334,13 +30335,13 @@ function packagePolicyId(packageName, versionRange) {
28334
30335
  }
28335
30336
  return void 0;
28336
30337
  }
28337
- function collectNpmDependencies(input) {
28338
- if ((0, import_posix2.basename)(input.path) !== "package.json") {
30338
+ function collectNpmDependencies(input2) {
30339
+ if ((0, import_posix2.basename)(input2.path) !== "package.json") {
28339
30340
  return [];
28340
30341
  }
28341
30342
  let parsed;
28342
30343
  try {
28343
- parsed = JSON.parse(input.text);
30344
+ parsed = JSON.parse(input2.text);
28344
30345
  } catch {
28345
30346
  return [];
28346
30347
  }
@@ -28364,17 +30365,17 @@ function collectNpmDependencies(input) {
28364
30365
  ecosystem: "npm",
28365
30366
  packageName,
28366
30367
  versionRange: rawVersion,
28367
- manifestPath: input.path,
30368
+ manifestPath: input2.path,
28368
30369
  policyId: packagePolicyId(packageName, rawVersion)
28369
30370
  });
28370
30371
  }
28371
30372
  }
28372
30373
  return facts;
28373
30374
  }
28374
- function collectLineBasedDependencies(input) {
28375
- const name = (0, import_posix2.basename)(input.path);
30375
+ function collectLineBasedDependencies(input2) {
30376
+ const name = (0, import_posix2.basename)(input2.path);
28376
30377
  const facts = [];
28377
- for (const line of input.text.split(/\r?\n/u)) {
30378
+ for (const line of input2.text.split(/\r?\n/u)) {
28378
30379
  const trimmed = line.trim();
28379
30380
  let match = null;
28380
30381
  let ecosystem;
@@ -28399,13 +30400,13 @@ function collectLineBasedDependencies(input) {
28399
30400
  ecosystem,
28400
30401
  packageName: match[1],
28401
30402
  versionRange: match[2],
28402
- manifestPath: input.path
30403
+ manifestPath: input2.path
28403
30404
  });
28404
30405
  }
28405
30406
  return facts;
28406
30407
  }
28407
- function collectRegexDependencies(input) {
28408
- const name = (0, import_posix2.basename)(input.path);
30408
+ function collectRegexDependencies(input2) {
30409
+ const name = (0, import_posix2.basename)(input2.path);
28409
30410
  const facts = [];
28410
30411
  let ecosystem;
28411
30412
  let pattern;
@@ -28422,7 +30423,7 @@ function collectRegexDependencies(input) {
28422
30423
  if (!ecosystem || !pattern) {
28423
30424
  return facts;
28424
30425
  }
28425
- for (const match of input.text.matchAll(pattern)) {
30426
+ for (const match of input2.text.matchAll(pattern)) {
28426
30427
  const packageName = match[1] ?? match[3];
28427
30428
  const versionRange = match[2] ?? match[4] ?? match[5];
28428
30429
  if (!packageName || !versionRange) {
@@ -28432,7 +30433,7 @@ function collectRegexDependencies(input) {
28432
30433
  ecosystem,
28433
30434
  packageName,
28434
30435
  versionRange,
28435
- manifestPath: input.path
30436
+ manifestPath: input2.path
28436
30437
  });
28437
30438
  }
28438
30439
  return facts;
@@ -28442,10 +30443,10 @@ function isDependencyManifestPath(path) {
28442
30443
  return name === "package.json" || name === "package-lock.json" || name === "yarn.lock" || name === "pnpm-lock.yaml" || name === "go.mod" || name === "pom.xml" || name.endsWith(".gradle") || name === "composer.json" || name === "composer.lock" || name === "requirements.txt" || name === "requirements.lock" || name === "Gemfile" || name === "Gemfile.lock" || name === "Cargo.toml" || name === "Cargo.lock";
28443
30444
  }
28444
30445
  function collectProjectDependencyFacts(manifests) {
28445
- return manifests.flatMap((input) => [
28446
- ...collectNpmDependencies(input),
28447
- ...collectLineBasedDependencies(input),
28448
- ...collectRegexDependencies(input)
30446
+ return manifests.flatMap((input2) => [
30447
+ ...collectNpmDependencies(input2),
30448
+ ...collectLineBasedDependencies(input2),
30449
+ ...collectRegexDependencies(input2)
28449
30450
  ]);
28450
30451
  }
28451
30452
  function appendDependencyFacts(analyzedFiles, dependencyFacts) {
@@ -29279,15 +31280,15 @@ function augmentProjectFacts(analyzedFiles, options) {
29279
31280
  var CHECK_ENGINE_KIND = "critiq-cli";
29280
31281
  function loadCheckEngineVersion() {
29281
31282
  const candidatePaths = [
29282
- (0, import_node_path10.resolve)(__dirname, "./package.json"),
29283
- (0, import_node_path10.resolve)(__dirname, "../package.json"),
29284
- (0, import_node_path10.resolve)(__dirname, "../../../package.json")
31283
+ (0, import_node_path12.resolve)(__dirname, "./package.json"),
31284
+ (0, import_node_path12.resolve)(__dirname, "../package.json"),
31285
+ (0, import_node_path12.resolve)(__dirname, "../../../package.json")
29285
31286
  ];
29286
31287
  try {
29287
31288
  for (const candidatePath of candidatePaths) {
29288
31289
  try {
29289
31290
  const packageJson = JSON.parse(
29290
- (0, import_node_fs8.readFileSync)(candidatePath, "utf8")
31291
+ (0, import_node_fs9.readFileSync)(candidatePath, "utf8")
29291
31292
  );
29292
31293
  if (packageJson.version && packageJson.version.trim().length > 0) {
29293
31294
  return packageJson.version.trim();
@@ -29352,8 +31353,8 @@ function resolveCatalogPackageRuntime(displayRoot, packageName, options) {
29352
31353
  data: {
29353
31354
  packageName,
29354
31355
  packageRoot: packageRootOverride,
29355
- entryPath: (0, import_node_path10.resolve)(packageRootOverride, "package.json"),
29356
- catalogPath: (0, import_node_path10.resolve)(
31356
+ entryPath: (0, import_node_path12.resolve)(packageRootOverride, "package.json"),
31357
+ catalogPath: (0, import_node_path12.resolve)(
29357
31358
  packageRootOverride,
29358
31359
  RULE_CATALOG_FILENAME
29359
31360
  )
@@ -29619,7 +31620,7 @@ function runCheckCommand(options = {}) {
29619
31620
  continue;
29620
31621
  }
29621
31622
  sourceTextsByPath.set(displayPath, textResult.text);
29622
- const adapter = registry.findAdapterForPath(displayPath);
31623
+ const adapter = registry.findAdapterForPath(displayPath, textResult.text);
29623
31624
  if (!adapter) {
29624
31625
  diagnostics.push(
29625
31626
  createCheckRuntimeDiagnostic(
@@ -29649,6 +31650,9 @@ function runCheckCommand(options = {}) {
29649
31650
  });
29650
31651
  continue;
29651
31652
  }
31653
+ if (analysis.diagnostics?.length) {
31654
+ diagnostics.push(...analysis.diagnostics);
31655
+ }
29652
31656
  analyzedFiles.push({
29653
31657
  ...analysis.data,
29654
31658
  changedRanges: filteredScope.changedRangesByAbsolutePath.get(absolutePath)
@@ -29764,7 +31768,7 @@ function runCheckCommand(options = {}) {
29764
31768
  }
29765
31769
 
29766
31770
  // libs/runtime/check-runner/src/lib/secrets-scanner/run-secrets-scan.ts
29767
- var import_node_fs9 = require("node:fs");
31771
+ var import_node_fs10 = require("node:fs");
29768
31772
 
29769
31773
  // libs/runtime/check-runner/src/lib/secrets-scanner/detectors.ts
29770
31774
  var import_node_crypto3 = require("node:crypto");
@@ -30097,7 +32101,7 @@ function runSecretsScan(options = {}) {
30097
32101
  } else {
30098
32102
  let size = 0;
30099
32103
  try {
30100
- size = (0, import_node_fs9.statSync)(absolutePath).size;
32104
+ size = (0, import_node_fs10.statSync)(absolutePath).size;
30101
32105
  } catch {
30102
32106
  continue;
30103
32107
  }
@@ -30175,8 +32179,8 @@ function toCheckSecretsScanPayload(result) {
30175
32179
  }
30176
32180
 
30177
32181
  // apps/cli/src/commands/audit-secrets.command.ts
30178
- var import_node_fs10 = require("node:fs");
30179
- var import_node_path11 = require("node:path");
32182
+ var import_node_fs11 = require("node:fs");
32183
+ var import_node_path13 = require("node:path");
30180
32184
 
30181
32185
  // apps/cli/src/rendering/check/terminal-frames.rendering.ts
30182
32186
  var ansi = {
@@ -30501,9 +32505,9 @@ function collectSourceTextsForFindings(displayRoot, result) {
30501
32505
  }
30502
32506
  const map = /* @__PURE__ */ new Map();
30503
32507
  for (const displayPath of paths) {
30504
- const absolutePath = (0, import_node_path11.isAbsolute)(displayPath) ? displayPath : (0, import_node_path11.resolve)(displayRoot, displayPath);
32508
+ const absolutePath = (0, import_node_path13.isAbsolute)(displayPath) ? displayPath : (0, import_node_path13.resolve)(displayRoot, displayPath);
30505
32509
  try {
30506
- map.set(displayPath, (0, import_node_fs10.readFileSync)(absolutePath, "utf8"));
32510
+ map.set(displayPath, (0, import_node_fs11.readFileSync)(absolutePath, "utf8"));
30507
32511
  } catch {
30508
32512
  }
30509
32513
  }
@@ -30803,8 +32807,597 @@ function renderCheckSarif(envelope) {
30803
32807
  );
30804
32808
  }
30805
32809
 
32810
+ // apps/cli/src/utils/ensure-catalog-package.util.ts
32811
+ var import_node_fs16 = require("node:fs");
32812
+ var import_node_path18 = require("node:path");
32813
+
32814
+ // apps/cli/src/utils/detect-cli-install-scope.util.ts
32815
+ var import_node_fs13 = require("node:fs");
32816
+ var import_node_path15 = require("node:path");
32817
+
32818
+ // apps/cli/src/utils/node-modules-roots.util.ts
32819
+ var import_node_child_process3 = require("node:child_process");
32820
+ var import_node_fs12 = require("node:fs");
32821
+ var import_node_path14 = require("node:path");
32822
+ function tryCommandOutput(command, args) {
32823
+ try {
32824
+ return (0, import_node_child_process3.execFileSync)(command, args, {
32825
+ encoding: "utf8",
32826
+ stdio: ["ignore", "pipe", "ignore"]
32827
+ }).trim();
32828
+ } catch {
32829
+ return null;
32830
+ }
32831
+ }
32832
+ function getRepoNodeModulesRoot(cwd) {
32833
+ return (0, import_node_path14.resolve)(cwd, "node_modules");
32834
+ }
32835
+ function getGlobalNodeModulesRoots() {
32836
+ const roots = /* @__PURE__ */ new Set();
32837
+ const npmRoot = tryCommandOutput("npm", ["root", "-g"]);
32838
+ if (npmRoot) {
32839
+ roots.add(npmRoot);
32840
+ }
32841
+ const pnpmRoot = tryCommandOutput("pnpm", ["root", "-g"]);
32842
+ if (pnpmRoot) {
32843
+ roots.add(pnpmRoot);
32844
+ }
32845
+ const yarnGlobalDir = tryCommandOutput("yarn", ["global", "dir"]);
32846
+ if (yarnGlobalDir) {
32847
+ roots.add((0, import_node_path14.resolve)(yarnGlobalDir, "node_modules"));
32848
+ }
32849
+ return [...roots].filter((root) => (0, import_node_fs12.existsSync)(root));
32850
+ }
32851
+
32852
+ // apps/cli/src/utils/detect-cli-install-scope.util.ts
32853
+ function safeRealpath(path) {
32854
+ try {
32855
+ return (0, import_node_fs13.realpathSync)(path);
32856
+ } catch {
32857
+ return null;
32858
+ }
32859
+ }
32860
+ function normalizeEntryPath(entryPath) {
32861
+ if (!entryPath) {
32862
+ return null;
32863
+ }
32864
+ return safeRealpath(entryPath) ?? (0, import_node_path15.resolve)(entryPath);
32865
+ }
32866
+ function detectCliInstallScope(cwd, entryPath = process.argv[1]) {
32867
+ const resolvedEntryPath = normalizeEntryPath(entryPath);
32868
+ if (!resolvedEntryPath) {
32869
+ return "external";
32870
+ }
32871
+ const repoNodeModules = safeRealpath(getRepoNodeModulesRoot(cwd));
32872
+ if (repoNodeModules) {
32873
+ const localCliMain = safeRealpath(
32874
+ (0, import_node_path15.resolve)(repoNodeModules, "@critiq/cli/main.js")
32875
+ );
32876
+ const localBin = safeRealpath((0, import_node_path15.resolve)(repoNodeModules, ".bin/critiq"));
32877
+ if (resolvedEntryPath === localCliMain || resolvedEntryPath === localBin || resolvedEntryPath.startsWith(`${(0, import_node_path15.resolve)(repoNodeModules, "@critiq/cli")}${import_node_path15.sep}`)) {
32878
+ return "local";
32879
+ }
32880
+ }
32881
+ for (const globalRoot of getGlobalNodeModulesRoots()) {
32882
+ const globalCliMain = (0, import_node_path15.resolve)(globalRoot, "@critiq/cli/main.js");
32883
+ const globalBin = (0, import_node_path15.resolve)(globalRoot, ".bin/critiq");
32884
+ if (resolvedEntryPath === safeRealpath(globalCliMain) || resolvedEntryPath === safeRealpath(globalBin) || resolvedEntryPath.startsWith(`${(0, import_node_path15.resolve)(globalRoot, "@critiq/cli")}${import_node_path15.sep}`)) {
32885
+ return "global";
32886
+ }
32887
+ }
32888
+ if (resolvedEntryPath.includes(`${import_node_path15.sep}node_modules${import_node_path15.sep}`)) {
32889
+ return "external";
32890
+ }
32891
+ if ((0, import_node_fs13.existsSync)(resolvedEntryPath)) {
32892
+ return "external";
32893
+ }
32894
+ return "external";
32895
+ }
32896
+
32897
+ // apps/cli/src/utils/detect-package-manager.util.ts
32898
+ var import_node_fs14 = require("node:fs");
32899
+ var import_node_path16 = require("node:path");
32900
+ function readPackageManagerField(cwd) {
32901
+ const packageJsonPath = (0, import_node_path16.join)(cwd, "package.json");
32902
+ if (!(0, import_node_fs14.existsSync)(packageJsonPath)) {
32903
+ return null;
32904
+ }
32905
+ try {
32906
+ const manifest = JSON.parse((0, import_node_fs14.readFileSync)(packageJsonPath, "utf8"));
32907
+ const value = manifest.packageManager?.trim();
32908
+ if (!value) {
32909
+ return null;
32910
+ }
32911
+ if (value.startsWith("pnpm@")) {
32912
+ return "pnpm";
32913
+ }
32914
+ if (value.startsWith("yarn@")) {
32915
+ return "yarn";
32916
+ }
32917
+ if (value.startsWith("npm@")) {
32918
+ return "npm";
32919
+ }
32920
+ if (value.startsWith("bun@")) {
32921
+ return "bun";
32922
+ }
32923
+ } catch {
32924
+ return null;
32925
+ }
32926
+ return null;
32927
+ }
32928
+ function detectPackageManager(cwd) {
32929
+ const fromField = readPackageManagerField(cwd);
32930
+ if (fromField) {
32931
+ return fromField;
32932
+ }
32933
+ if ((0, import_node_fs14.existsSync)((0, import_node_path16.join)(cwd, "bun.lockb")) || (0, import_node_fs14.existsSync)((0, import_node_path16.join)(cwd, "bun.lock"))) {
32934
+ return "bun";
32935
+ }
32936
+ if ((0, import_node_fs14.existsSync)((0, import_node_path16.join)(cwd, "pnpm-lock.yaml"))) {
32937
+ return "pnpm";
32938
+ }
32939
+ if ((0, import_node_fs14.existsSync)((0, import_node_path16.join)(cwd, "yarn.lock"))) {
32940
+ return "yarn";
32941
+ }
32942
+ if ((0, import_node_fs14.existsSync)((0, import_node_path16.join)(cwd, "package-lock.json")) || (0, import_node_fs14.existsSync)((0, import_node_path16.join)(cwd, "npm-shrinkwrap.json"))) {
32943
+ return "npm";
32944
+ }
32945
+ return "npm";
32946
+ }
32947
+ function buildCatalogInstallCommand(packageManager, packageName, scope) {
32948
+ switch (packageManager) {
32949
+ case "yarn":
32950
+ if (scope === "global") {
32951
+ return {
32952
+ executable: "yarn",
32953
+ args: ["global", "add", packageName],
32954
+ display: `yarn global add ${packageName}`
32955
+ };
32956
+ }
32957
+ return {
32958
+ executable: "yarn",
32959
+ args: ["add", "-D", packageName],
32960
+ display: `yarn add -D ${packageName}`
32961
+ };
32962
+ case "pnpm":
32963
+ if (scope === "global") {
32964
+ return {
32965
+ executable: "pnpm",
32966
+ args: ["add", "-g", packageName],
32967
+ display: `pnpm add -g ${packageName}`
32968
+ };
32969
+ }
32970
+ return {
32971
+ executable: "pnpm",
32972
+ args: ["add", "-D", packageName],
32973
+ display: `pnpm add -D ${packageName}`
32974
+ };
32975
+ case "bun":
32976
+ if (scope === "global") {
32977
+ return {
32978
+ executable: "bun",
32979
+ args: ["add", "-g", packageName],
32980
+ display: `bun add -g ${packageName}`
32981
+ };
32982
+ }
32983
+ return {
32984
+ executable: "bun",
32985
+ args: ["add", "-d", packageName],
32986
+ display: `bun add -d ${packageName}`
32987
+ };
32988
+ case "npm":
32989
+ if (scope === "global") {
32990
+ return {
32991
+ executable: "npm",
32992
+ args: ["install", "-g", packageName],
32993
+ display: `npm install -g ${packageName}`
32994
+ };
32995
+ }
32996
+ return {
32997
+ executable: "npm",
32998
+ args: ["install", "-D", packageName],
32999
+ display: `npm install -D ${packageName}`
33000
+ };
33001
+ }
33002
+ }
33003
+
33004
+ // apps/cli/src/utils/format-catalog-not-found-message.util.ts
33005
+ function formatSuggestedInstallCommands(cwd, packageName) {
33006
+ const packageManager = detectPackageManager(cwd);
33007
+ const localCommand = buildCatalogInstallCommand(
33008
+ packageManager,
33009
+ packageName,
33010
+ "local"
33011
+ );
33012
+ const globalCommand = buildCatalogInstallCommand(
33013
+ packageManager,
33014
+ packageName,
33015
+ "global"
33016
+ );
33017
+ const oneShotCommand = `npx -p @critiq/cli -p ${packageName} critiq check .`;
33018
+ return [
33019
+ `Install in this repository: ${localCommand.display}`,
33020
+ `Install globally: ${globalCommand.display}`,
33021
+ `Run once without installing: ${oneShotCommand}`
33022
+ ];
33023
+ }
33024
+ function formatCatalogPackageNotFoundMessage(input2) {
33025
+ const lines = [
33026
+ `Critiq could not find the rules catalog package \`${input2.packageName}\`.`
33027
+ ];
33028
+ if (input2.cliScope === "local") {
33029
+ lines.push(
33030
+ "The CLI is installed in this repository, but the rules catalog is missing from `./node_modules`."
33031
+ );
33032
+ } else if (input2.cliScope === "global") {
33033
+ lines.push(
33034
+ "The CLI is installed globally. Critiq checked this repository and your global Node modules, but the rules catalog is not installed in either place."
33035
+ );
33036
+ } else {
33037
+ lines.push(
33038
+ "Critiq checked this repository and your global Node modules, but the rules catalog is not installed in either place."
33039
+ );
33040
+ }
33041
+ if (input2.includeInstallSuggestions) {
33042
+ lines.push("", "You can install the default OSS rules catalog with:");
33043
+ for (const suggestion of formatSuggestedInstallCommands(
33044
+ input2.cwd,
33045
+ input2.packageName
33046
+ )) {
33047
+ lines.push(` ${suggestion}`);
33048
+ }
33049
+ } else {
33050
+ lines.push(
33051
+ "",
33052
+ `Install the catalog package manually, or point \`catalog.package\` in \`.critiq/config.yaml\` at the package you want to use.`
33053
+ );
33054
+ }
33055
+ lines.push(
33056
+ "",
33057
+ "For CI or non-interactive runs, install the catalog before running `critiq check`."
33058
+ );
33059
+ return lines.join("\n");
33060
+ }
33061
+ function formatInstallCancelledMessage(packageName) {
33062
+ return `Cancelled. \`${packageName}\` was not installed, so the scan did not run.`;
33063
+ }
33064
+ function formatInstallFailureMessage(command) {
33065
+ return `Failed to install the rules catalog with \`${command.display}\`. Fix the error above and try again.`;
33066
+ }
33067
+ function formatInstallSuccessMessage(command) {
33068
+ return `Installed the rules catalog with \`${command.display}\`. Continuing with the scan...`;
33069
+ }
33070
+ function formatLocalInstallUnavailableMessage() {
33071
+ return "A local install requires a `package.json` in this repository. Choose global install instead, or create a package manifest first.";
33072
+ }
33073
+
33074
+ // apps/cli/src/utils/prompt.util.ts
33075
+ var import_promises = require("node:readline/promises");
33076
+ var import_node_process = require("node:process");
33077
+ async function promptChoice(promptInput) {
33078
+ const lines = [promptInput.title, ""];
33079
+ for (const option of promptInput.options) {
33080
+ const suffix = option.id === promptInput.defaultOptionId ? " (default)" : "";
33081
+ lines.push(` [${option.id}] ${option.label}${suffix}`);
33082
+ }
33083
+ lines.push("", "Enter a choice and press Return.");
33084
+ const readline = (0, import_promises.createInterface)({
33085
+ input: import_node_process.stdin,
33086
+ output: import_node_process.stdout,
33087
+ terminal: true
33088
+ });
33089
+ try {
33090
+ const answer = (await readline.question(`${lines.join("\n")}
33091
+ > `)).trim();
33092
+ if (answer.length === 0 && promptInput.defaultOptionId) {
33093
+ return promptInput.defaultOptionId;
33094
+ }
33095
+ const matched = promptInput.options.find((option) => option.id === answer);
33096
+ return matched?.id ?? null;
33097
+ } finally {
33098
+ readline.close();
33099
+ }
33100
+ }
33101
+
33102
+ // apps/cli/src/utils/probe-catalog-package.util.ts
33103
+ function probeFromBasePath(cwd, packageName, basePath) {
33104
+ const result = resolveCatalogPackage(cwd, packageName, [basePath]);
33105
+ if (!result.success) {
33106
+ return null;
33107
+ }
33108
+ return result.data.packageRoot;
33109
+ }
33110
+ function probeCatalogPackageInRepo(cwd, packageName) {
33111
+ return probeFromBasePath(cwd, packageName, getRepoNodeModulesRoot(cwd));
33112
+ }
33113
+ function probeCatalogPackageGlobally(cwd, packageName) {
33114
+ for (const globalRoot of getGlobalNodeModulesRoots()) {
33115
+ const packageRoot = probeFromBasePath(cwd, packageName, globalRoot);
33116
+ if (packageRoot) {
33117
+ return packageRoot;
33118
+ }
33119
+ }
33120
+ return null;
33121
+ }
33122
+ function probeCatalogPackageResolution(cwd, packageName, options = {}) {
33123
+ const repoPackageRoot = probeCatalogPackageInRepo(cwd, packageName);
33124
+ if (repoPackageRoot) {
33125
+ return {
33126
+ packageName,
33127
+ location: "repo",
33128
+ packageRoot: repoPackageRoot
33129
+ };
33130
+ }
33131
+ if (options.includeGlobal !== false) {
33132
+ const globalPackageRoot = probeCatalogPackageGlobally(cwd, packageName);
33133
+ if (globalPackageRoot) {
33134
+ return {
33135
+ packageName,
33136
+ location: "global",
33137
+ packageRoot: globalPackageRoot
33138
+ };
33139
+ }
33140
+ }
33141
+ return null;
33142
+ }
33143
+
33144
+ // apps/cli/src/utils/resolve-catalog-package-name.util.ts
33145
+ var import_node_fs15 = require("node:fs");
33146
+ var import_node_path17 = require("node:path");
33147
+ function resolveCatalogPackageNameForEnsure(cwd) {
33148
+ const loaded = loadCritiqConfigForDirectory(cwd);
33149
+ if (!loaded.success) {
33150
+ const firstCode = loaded.diagnostics[0]?.code;
33151
+ if (firstCode === "config.file.not-found") {
33152
+ return DEFAULT_CATALOG_PACKAGE_NAME;
33153
+ }
33154
+ return null;
33155
+ }
33156
+ return loaded.data.catalogPackage ?? DEFAULT_CATALOG_PACKAGE_NAME;
33157
+ }
33158
+ function repoHasPackageJson(cwd) {
33159
+ return (0, import_node_fs15.existsSync)((0, import_node_path17.join)(cwd, "package.json"));
33160
+ }
33161
+ function canInstallCatalogLocally(cwd) {
33162
+ return repoHasPackageJson(cwd);
33163
+ }
33164
+ function isDefaultInstallableCatalogPackage(packageName) {
33165
+ return packageName === DEFAULT_CATALOG_PACKAGE_NAME;
33166
+ }
33167
+
33168
+ // apps/cli/src/utils/run-package-install.util.ts
33169
+ var import_node_child_process4 = require("node:child_process");
33170
+ function runPackageInstall(options) {
33171
+ options.writeStdout(`Running: ${options.command.display}`);
33172
+ try {
33173
+ (0, import_node_child_process4.execFileSync)(options.command.executable, [...options.command.args], {
33174
+ cwd: options.cwd,
33175
+ stdio: "inherit"
33176
+ });
33177
+ return true;
33178
+ } catch {
33179
+ options.writeStderr(
33180
+ `Command failed: ${options.command.display}`
33181
+ );
33182
+ return false;
33183
+ }
33184
+ }
33185
+
33186
+ // apps/cli/src/utils/ensure-catalog-package.util.ts
33187
+ function buildCatalogRuntimeOptions(packageName, packageRoot, cwd) {
33188
+ if (probeCatalogPackageInRepo(cwd, packageName)) {
33189
+ return {
33190
+ ok: true,
33191
+ catalogResolverBasePaths: [cwd]
33192
+ };
33193
+ }
33194
+ return {
33195
+ ok: true,
33196
+ catalogPackageRoots: {
33197
+ [packageName]: packageRoot
33198
+ },
33199
+ catalogResolverBasePaths: [cwd]
33200
+ };
33201
+ }
33202
+ function failure(message, exitCode = 1) {
33203
+ return {
33204
+ ok: false,
33205
+ exitCode,
33206
+ message
33207
+ };
33208
+ }
33209
+ function shouldOfferInteractiveInstall(runtime, format) {
33210
+ if (format !== "pretty") {
33211
+ return false;
33212
+ }
33213
+ if (!runtime.isInteractive) {
33214
+ return false;
33215
+ }
33216
+ if (process.env["CI"]?.trim()) {
33217
+ return false;
33218
+ }
33219
+ return true;
33220
+ }
33221
+ async function installCatalogPackage(runtime, packageName, scope) {
33222
+ const packageManager = detectPackageManager(runtime.cwd);
33223
+ const command = buildCatalogInstallCommand(
33224
+ packageManager,
33225
+ packageName,
33226
+ scope
33227
+ );
33228
+ if (runtime.runPackageInstall) {
33229
+ return runtime.runPackageInstall({
33230
+ cwd: scope === "local" ? runtime.cwd : void 0,
33231
+ command
33232
+ });
33233
+ }
33234
+ return runPackageInstall({
33235
+ cwd: scope === "local" ? runtime.cwd : void 0,
33236
+ command,
33237
+ writeStdout: runtime.writeStdout,
33238
+ writeStderr: runtime.writeStderr
33239
+ });
33240
+ }
33241
+ async function promptForInstallChoice(runtime, packageName, cliScope) {
33242
+ if (runtime.promptChoice) {
33243
+ if (cliScope === "local") {
33244
+ return runtime.promptChoice({
33245
+ title: `The rules catalog \`${packageName}\` is not installed in this repository.`,
33246
+ options: [
33247
+ { id: "local", label: "Install in this repository" },
33248
+ { id: "cancel", label: "Cancel the scan" }
33249
+ ],
33250
+ defaultOptionId: "local"
33251
+ });
33252
+ }
33253
+ const options2 = [
33254
+ ...canInstallCatalogLocally(runtime.cwd) ? [{ id: "local", label: "Install in this repository" }] : [],
33255
+ { id: "global", label: "Install globally" },
33256
+ { id: "cancel", label: "Cancel the scan" }
33257
+ ];
33258
+ return runtime.promptChoice({
33259
+ title: `The rules catalog \`${packageName}\` is not installed.`,
33260
+ options: [...options2],
33261
+ defaultOptionId: canInstallCatalogLocally(runtime.cwd) ? "local" : "global"
33262
+ });
33263
+ }
33264
+ if (cliScope === "local") {
33265
+ const choice2 = await promptChoice({
33266
+ title: `The rules catalog \`${packageName}\` is not installed in this repository.`,
33267
+ options: [
33268
+ { id: "local", label: "Install in this repository" },
33269
+ { id: "cancel", label: "Cancel the scan" }
33270
+ ],
33271
+ defaultOptionId: "local"
33272
+ });
33273
+ return choice2;
33274
+ }
33275
+ const options = [
33276
+ ...canInstallCatalogLocally(runtime.cwd) ? [{ id: "local", label: "Install in this repository" }] : [],
33277
+ { id: "global", label: "Install globally" },
33278
+ { id: "cancel", label: "Cancel the scan" }
33279
+ ];
33280
+ const choice = await promptChoice({
33281
+ title: `The rules catalog \`${packageName}\` is not installed.`,
33282
+ options,
33283
+ defaultOptionId: canInstallCatalogLocally(runtime.cwd) ? "local" : "global"
33284
+ });
33285
+ return choice;
33286
+ }
33287
+ async function handleMissingCatalogPackage(runtime, packageName, cliScope, includeInstallSuggestions, format) {
33288
+ if (!shouldOfferInteractiveInstall(runtime, format)) {
33289
+ return failure(
33290
+ formatCatalogPackageNotFoundMessage({
33291
+ cwd: runtime.cwd,
33292
+ packageName,
33293
+ cliScope,
33294
+ includeInstallSuggestions
33295
+ })
33296
+ );
33297
+ }
33298
+ const choice = await promptForInstallChoice(runtime, packageName, cliScope);
33299
+ if (choice === null || choice === "cancel") {
33300
+ return failure(formatInstallCancelledMessage(packageName));
33301
+ }
33302
+ if (choice === "local" && !canInstallCatalogLocally(runtime.cwd)) {
33303
+ runtime.writeStderr(formatLocalInstallUnavailableMessage());
33304
+ return failure(formatInstallCancelledMessage(packageName));
33305
+ }
33306
+ const packageManager = detectPackageManager(runtime.cwd);
33307
+ const command = buildCatalogInstallCommand(
33308
+ packageManager,
33309
+ packageName,
33310
+ choice
33311
+ );
33312
+ runtime.writeStdout(`About to run: ${command.display}`);
33313
+ const installed = await installCatalogPackage(runtime, packageName, choice);
33314
+ if (!installed) {
33315
+ return failure(formatInstallFailureMessage(command));
33316
+ }
33317
+ runtime.writeStdout(formatInstallSuccessMessage(command));
33318
+ const packageRoot = choice === "local" ? probeCatalogPackageInRepo(runtime.cwd, packageName) : probeCatalogPackageGlobally(runtime.cwd, packageName);
33319
+ if (!packageRoot) {
33320
+ return failure(
33321
+ formatCatalogPackageNotFoundMessage({
33322
+ cwd: runtime.cwd,
33323
+ packageName,
33324
+ cliScope,
33325
+ includeInstallSuggestions
33326
+ })
33327
+ );
33328
+ }
33329
+ return buildCatalogRuntimeOptions(packageName, packageRoot, runtime.cwd);
33330
+ }
33331
+ function probeCatalogFromEnvironment() {
33332
+ const rulesRoot = process.env["CRITIQ_RULES_ROOT"]?.trim();
33333
+ if (!rulesRoot) {
33334
+ return null;
33335
+ }
33336
+ const candidateRoots = [
33337
+ (0, import_node_path18.resolve)(rulesRoot),
33338
+ (0, import_node_path18.resolve)(rulesRoot, "libs/rules/catalog")
33339
+ ];
33340
+ for (const candidateRoot of candidateRoots) {
33341
+ if ((0, import_node_fs16.existsSync)((0, import_node_path18.resolve)(candidateRoot, "catalog.yaml"))) {
33342
+ return candidateRoot;
33343
+ }
33344
+ }
33345
+ return null;
33346
+ }
33347
+ async function ensureCatalogPackageForCheck(runtime, format) {
33348
+ const packageName = resolveCatalogPackageNameForEnsure(runtime.cwd);
33349
+ if (!packageName) {
33350
+ return { ok: true };
33351
+ }
33352
+ const environmentCatalogRoot = probeCatalogFromEnvironment();
33353
+ if (environmentCatalogRoot) {
33354
+ return buildCatalogRuntimeOptions(
33355
+ packageName,
33356
+ environmentCatalogRoot,
33357
+ runtime.cwd
33358
+ );
33359
+ }
33360
+ const cliScope = runtime.cliInstallScope ?? detectCliInstallScope(runtime.cwd);
33361
+ const includeGlobalLookup = cliScope !== "local";
33362
+ const resolved = probeCatalogPackageResolution(runtime.cwd, packageName, {
33363
+ includeGlobal: includeGlobalLookup
33364
+ });
33365
+ if (resolved) {
33366
+ return buildCatalogRuntimeOptions(
33367
+ resolved.packageName,
33368
+ resolved.packageRoot,
33369
+ runtime.cwd
33370
+ );
33371
+ }
33372
+ const includeInstallSuggestions = isDefaultInstallableCatalogPackage(
33373
+ packageName
33374
+ );
33375
+ if (!includeInstallSuggestions || !shouldOfferInteractiveInstall(runtime, format)) {
33376
+ return failure(
33377
+ formatCatalogPackageNotFoundMessage({
33378
+ cwd: runtime.cwd,
33379
+ packageName,
33380
+ cliScope,
33381
+ includeInstallSuggestions
33382
+ })
33383
+ );
33384
+ }
33385
+ return handleMissingCatalogPackage(
33386
+ runtime,
33387
+ packageName,
33388
+ cliScope,
33389
+ includeInstallSuggestions,
33390
+ format
33391
+ );
33392
+ }
33393
+
30806
33394
  // apps/cli/src/commands/check.command.ts
30807
- function handleCheck(target, format, runtime, baseRef, headRef, staged = false) {
33395
+ async function handleCheck(target, format, runtime, baseRef, headRef, staged = false) {
33396
+ const catalogEnsure = await ensureCatalogPackageForCheck(runtime, format);
33397
+ if (catalogEnsure.ok === false) {
33398
+ runtime.writeStderr(catalogEnsure.message);
33399
+ return catalogEnsure.exitCode;
33400
+ }
30808
33401
  const progressRenderer = format === "pretty" ? createScanProgressRenderer(runtime) : null;
30809
33402
  const runnerFormat = format === "pretty" ? "pretty" : "json";
30810
33403
  const result = runCheckCommand({
@@ -30813,7 +33406,10 @@ function handleCheck(target, format, runtime, baseRef, headRef, staged = false)
30813
33406
  format: runnerFormat,
30814
33407
  baseRef,
30815
33408
  headRef,
30816
- catalogResolverBasePaths: [runtime.cwd],
33409
+ catalogResolverBasePaths: catalogEnsure.catalogResolverBasePaths ?? [
33410
+ runtime.cwd
33411
+ ],
33412
+ catalogPackageRoots: catalogEnsure.catalogPackageRoots,
30817
33413
  onProgress: progressRenderer ? (update) => {
30818
33414
  progressRenderer.update(update);
30819
33415
  } : void 0
@@ -30855,8 +33451,8 @@ function handleCheck(target, format, runtime, baseRef, headRef, staged = false)
30855
33451
  }
30856
33452
 
30857
33453
  // tools/testing/harness/src/lib/harness.ts
30858
- var import_node_fs11 = require("node:fs");
30859
- var import_node_path12 = require("node:path");
33454
+ var import_node_fs17 = require("node:fs");
33455
+ var import_node_path19 = require("node:path");
30860
33456
  var import_node_url4 = require("node:url");
30861
33457
  var import_zod8 = require("zod");
30862
33458
  var observedRangeSchema = import_zod8.z.object({
@@ -30967,7 +33563,7 @@ var ruleSpecSchema = import_zod8.z.object({
30967
33563
  fixtures: import_zod8.z.array(ruleSpecFixtureSchema).min(1)
30968
33564
  }).strict();
30969
33565
  function readTextFile(path) {
30970
- return (0, import_node_fs11.readFileSync)(path, "utf8");
33566
+ return (0, import_node_fs17.readFileSync)(path, "utf8");
30971
33567
  }
30972
33568
  function toPointer(path) {
30973
33569
  return path.length === 0 ? "/" : `/${path.join("/")}`;
@@ -31012,7 +33608,7 @@ function validateObservationFixture(fixturePath, text) {
31012
33608
  return validated.data;
31013
33609
  }
31014
33610
  function analyzeSourceFixture(fixturePath, text, adapterRegistry) {
31015
- const extension = (0, import_node_path12.extname)(fixturePath).toLowerCase();
33611
+ const extension = (0, import_node_path19.extname)(fixturePath).toLowerCase();
31016
33612
  const adapter = adapterRegistry.findAdapterForPath(fixturePath);
31017
33613
  if (!adapter) {
31018
33614
  return [
@@ -31056,8 +33652,8 @@ function analyzeWorkspaceFixture(workspaceRoot, adapterRegistry) {
31056
33652
  }
31057
33653
  const result = entry.adapter.analyze(entry.displayPath, textOrFailure);
31058
33654
  if (!result.success) {
31059
- const failure = result;
31060
- diagnostics.push(...failure.diagnostics);
33655
+ const failure2 = result;
33656
+ diagnostics.push(...failure2.diagnostics);
31061
33657
  continue;
31062
33658
  }
31063
33659
  analyzedFiles.push(result.data);
@@ -31181,20 +33777,20 @@ function assertFixtureExpectations(fixture, emittedFindings) {
31181
33777
  }
31182
33778
  return failures;
31183
33779
  }
31184
- function formatAssertionFailure(failure) {
31185
- const lines = [`- ${failure.assertion}: ${failure.message}`];
31186
- if (failure.expected !== void 0) {
31187
- lines.push(` expected: ${JSON.stringify(failure.expected)}`);
33780
+ function formatAssertionFailure(failure2) {
33781
+ const lines = [`- ${failure2.assertion}: ${failure2.message}`];
33782
+ if (failure2.expected !== void 0) {
33783
+ lines.push(` expected: ${JSON.stringify(failure2.expected)}`);
31188
33784
  }
31189
- if (failure.received !== void 0) {
31190
- lines.push(` received: ${JSON.stringify(failure.received)}`);
33785
+ if (failure2.received !== void 0) {
33786
+ lines.push(` received: ${JSON.stringify(failure2.received)}`);
31191
33787
  }
31192
33788
  return lines.join("\n");
31193
33789
  }
31194
33790
  function analyzeFixture(specDirectory, fixture, adapterRegistry) {
31195
33791
  const sourceKind = fixture.sourcePath ? "source" : fixture.observationPath ? "observation" : "workspace";
31196
33792
  const declaredPath = fixture.sourcePath ?? fixture.observationPath ?? fixture.workspacePath ?? "";
31197
- const fixturePath = (0, import_node_path12.resolve)(specDirectory, declaredPath);
33793
+ const fixturePath = (0, import_node_path19.resolve)(specDirectory, declaredPath);
31198
33794
  if (sourceKind === "workspace") {
31199
33795
  const workspaceAnalysis = analyzeWorkspaceFixture(fixturePath, adapterRegistry);
31200
33796
  return {
@@ -31226,8 +33822,8 @@ function analyzeFixture(specDirectory, fixture, adapterRegistry) {
31226
33822
  }
31227
33823
  };
31228
33824
  }
31229
- function validateRuleSpec(input) {
31230
- const result = ruleSpecSchema.safeParse(input);
33825
+ function validateRuleSpec(input2) {
33826
+ const result = ruleSpecSchema.safeParse(input2);
31231
33827
  if (result.success) {
31232
33828
  return {
31233
33829
  success: true,
@@ -31245,10 +33841,10 @@ function loadRuleSpec(path) {
31245
33841
  const uri = (0, import_node_url4.pathToFileURL)(path).href;
31246
33842
  const loaded = loadYamlText(text, uri);
31247
33843
  if (!loaded.success) {
31248
- const failure = loaded;
33844
+ const failure2 = loaded;
31249
33845
  return {
31250
33846
  success: false,
31251
- diagnostics: failure.issues.map(
33847
+ diagnostics: failure2.issues.map(
31252
33848
  (issue) => createDiagnostic({
31253
33849
  code: `harness.rulespec.${issue.kind}`,
31254
33850
  message: issue.message,
@@ -31260,10 +33856,10 @@ function loadRuleSpec(path) {
31260
33856
  }
31261
33857
  const validation = validateRuleSpec(loaded.data);
31262
33858
  if (!validation.success) {
31263
- const failure = validation;
33859
+ const failure2 = validation;
31264
33860
  return {
31265
33861
  success: false,
31266
- diagnostics: failure.issues.map(
33862
+ diagnostics: failure2.issues.map(
31267
33863
  (issue) => createDiagnostic({
31268
33864
  code: `harness.rulespec.${issue.code}`,
31269
33865
  message: issue.message,
@@ -31297,26 +33893,26 @@ function runRuleSpec(path, options = {}) {
31297
33893
  const adapterRegistry = options.adapterRegistry ?? createDefaultSourceAdapterRegistry();
31298
33894
  const loadedSpec = loadRuleSpec(path);
31299
33895
  if (!loadedSpec.success) {
31300
- const failure = loadedSpec;
33896
+ const failure2 = loadedSpec;
31301
33897
  return {
31302
33898
  specPath: path,
31303
33899
  rulePath: "",
31304
33900
  success: false,
31305
33901
  fixtureResults: [],
31306
- diagnostics: failure.diagnostics
33902
+ diagnostics: failure2.diagnostics
31307
33903
  };
31308
33904
  }
31309
- const specDirectory = (0, import_node_path12.dirname)(loadedSpec.data.path);
31310
- const absoluteRulePath = (0, import_node_path12.resolve)(specDirectory, loadedSpec.data.spec.rulePath);
33905
+ const specDirectory = (0, import_node_path19.dirname)(loadedSpec.data.path);
33906
+ const absoluteRulePath = (0, import_node_path19.resolve)(specDirectory, loadedSpec.data.spec.rulePath);
31311
33907
  const loadedRule = loadRuleFile(absoluteRulePath);
31312
33908
  if (!loadedRule.success) {
31313
- const failure = loadedRule;
33909
+ const failure2 = loadedRule;
31314
33910
  return {
31315
33911
  specPath: loadedSpec.data.path,
31316
33912
  rulePath: absoluteRulePath,
31317
33913
  success: false,
31318
33914
  fixtureResults: [],
31319
- diagnostics: failure.diagnostics
33915
+ diagnostics: failure2.diagnostics
31320
33916
  };
31321
33917
  }
31322
33918
  const validatedRule = validateLoadedRuleDocument(loadedRule.data);
@@ -31443,7 +34039,7 @@ function formatRuleSpecRunAsJson(result) {
31443
34039
  }
31444
34040
 
31445
34041
  // apps/cli/src/commands/rules.command.ts
31446
- var import_node_path14 = require("node:path");
34042
+ var import_node_path21 = require("node:path");
31447
34043
  var import_node_url5 = require("node:url");
31448
34044
 
31449
34045
  // apps/cli/src/rendering/rules.rendering.ts
@@ -31592,13 +34188,13 @@ function renderSingleFilePretty(heading, state) {
31592
34188
 
31593
34189
  // apps/cli/src/commands/rules-targets.ts
31594
34190
  var import_minimatch3 = require("minimatch");
31595
- var import_node_fs12 = require("node:fs");
31596
- var import_node_path13 = require("node:path");
34191
+ var import_node_fs18 = require("node:fs");
34192
+ var import_node_path20 = require("node:path");
31597
34193
  function resolveValidateTargets(cwd, target) {
31598
- const absoluteCandidate = (0, import_node_path13.resolve)(cwd, target);
34194
+ const absoluteCandidate = (0, import_node_path20.resolve)(cwd, target);
31599
34195
  if (!hasGlobMagic(target)) {
31600
34196
  try {
31601
- if (!(0, import_node_fs12.statSync)(absoluteCandidate).isFile()) {
34197
+ if (!(0, import_node_fs18.statSync)(absoluteCandidate).isFile()) {
31602
34198
  return {
31603
34199
  success: false,
31604
34200
  diagnostics: [
@@ -31667,11 +34263,11 @@ function resolveTestTargets(cwd, target) {
31667
34263
  const resolvedTarget = target ?? "**/*.spec.yaml";
31668
34264
  const resolved = resolveValidateTargets(cwd, resolvedTarget);
31669
34265
  if (!resolved.success) {
31670
- const failure = resolved;
34266
+ const failure2 = resolved;
31671
34267
  return {
31672
34268
  success: false,
31673
34269
  target: resolvedTarget,
31674
- diagnostics: failure.diagnostics
34270
+ diagnostics: failure2.diagnostics
31675
34271
  };
31676
34272
  }
31677
34273
  return {
@@ -31693,7 +34289,7 @@ function resolveSingleFilePath(cwd, inputPath) {
31693
34289
  }
31694
34290
  return {
31695
34291
  success: true,
31696
- absolutePath: (0, import_node_path13.resolve)(cwd, inputPath)
34292
+ absolutePath: (0, import_node_path20.resolve)(cwd, inputPath)
31697
34293
  };
31698
34294
  }
31699
34295
 
@@ -31724,12 +34320,12 @@ function validateResultForFileSafe(absolutePath, cwd) {
31724
34320
  const uri = (0, import_node_url5.pathToFileURL)(absolutePath).href;
31725
34321
  const loaded = loadRuleFile(absolutePath);
31726
34322
  if (!loaded.success) {
31727
- const failure = loaded;
34323
+ const failure2 = loaded;
31728
34324
  return {
31729
34325
  path: displayPath,
31730
34326
  uri,
31731
34327
  success: false,
31732
- diagnostics: failure.diagnostics
34328
+ diagnostics: failure2.diagnostics
31733
34329
  };
31734
34330
  }
31735
34331
  const validated = validateLoadedRuleDocument(loaded.data);
@@ -31756,19 +34352,19 @@ function buildSingleFileState(absolutePath, cwd) {
31756
34352
  const loadResult = loadRuleFile(absolutePath);
31757
34353
  parsedSummary.phases.load = loadResult.success ? "success" : "failure";
31758
34354
  if (!loadResult.success) {
31759
- const failure = loadResult;
34355
+ const failure2 = loadResult;
31760
34356
  return {
31761
34357
  displayPath,
31762
34358
  uri,
31763
34359
  parsedSummary,
31764
34360
  semanticStatus: {
31765
34361
  success: false,
31766
- diagnostics: failure.diagnostics
34362
+ diagnostics: failure2.diagnostics
31767
34363
  },
31768
34364
  normalizedRule: null,
31769
34365
  ruleHash: null,
31770
34366
  templateVariables,
31771
- diagnostics: failure.diagnostics
34367
+ diagnostics: failure2.diagnostics
31772
34368
  };
31773
34369
  }
31774
34370
  const contractValidation = validateLoadedRuleDocumentContract(
@@ -31776,19 +34372,19 @@ function buildSingleFileState(absolutePath, cwd) {
31776
34372
  );
31777
34373
  parsedSummary.phases.contractValidation = contractValidation.success ? "success" : "failure";
31778
34374
  if (!contractValidation.success) {
31779
- const failure = contractValidation;
34375
+ const failure2 = contractValidation;
31780
34376
  return {
31781
34377
  displayPath,
31782
34378
  uri,
31783
34379
  parsedSummary,
31784
34380
  semanticStatus: {
31785
34381
  success: false,
31786
- diagnostics: failure.diagnostics
34382
+ diagnostics: failure2.diagnostics
31787
34383
  },
31788
34384
  normalizedRule: null,
31789
34385
  ruleHash: null,
31790
34386
  templateVariables,
31791
- diagnostics: failure.diagnostics
34387
+ diagnostics: failure2.diagnostics
31792
34388
  };
31793
34389
  }
31794
34390
  const explainSummary = summarizeValidatedRuleDocument(
@@ -31838,7 +34434,7 @@ function buildSingleFileState(absolutePath, cwd) {
31838
34434
  };
31839
34435
  }
31840
34436
  function createInvalidSingleFileState(cwd, inputPath, diagnostics) {
31841
- const uri = (0, import_node_url5.pathToFileURL)((0, import_node_path14.resolve)(cwd, inputPath)).href;
34437
+ const uri = (0, import_node_url5.pathToFileURL)((0, import_node_path21.resolve)(cwd, inputPath)).href;
31842
34438
  return {
31843
34439
  displayPath: inputPath,
31844
34440
  uri,
@@ -31856,15 +34452,15 @@ function createInvalidSingleFileState(cwd, inputPath, diagnostics) {
31856
34452
  function handleValidate(target, format, runtime) {
31857
34453
  const resolved = resolveValidateTargets(runtime.cwd, target);
31858
34454
  if (!resolved.success) {
31859
- const failure = resolved;
31860
- const exitCode2 = determineExitCode(failure.diagnostics);
34455
+ const failure2 = resolved;
34456
+ const exitCode2 = determineExitCode(failure2.diagnostics);
31861
34457
  const envelope2 = {
31862
34458
  command: "rules.validate",
31863
34459
  format,
31864
34460
  target,
31865
34461
  matchedFileCount: 0,
31866
34462
  results: [],
31867
- diagnostics: aggregateDiagnostics(failure.diagnostics),
34463
+ diagnostics: aggregateDiagnostics(failure2.diagnostics),
31868
34464
  exitCode: exitCode2 === 0 ? 1 : exitCode2
31869
34465
  };
31870
34466
  if (format === "json") {
@@ -31900,15 +34496,15 @@ function handleValidate(target, format, runtime) {
31900
34496
  function handleTest(target, format, runtime) {
31901
34497
  const resolved = resolveTestTargets(runtime.cwd, target);
31902
34498
  if (!resolved.success) {
31903
- const failure = resolved;
31904
- const exitCode2 = determineExitCode(failure.diagnostics) || 1;
34499
+ const failure2 = resolved;
34500
+ const exitCode2 = determineExitCode(failure2.diagnostics) || 1;
31905
34501
  const envelope2 = {
31906
34502
  command: "rules.test",
31907
34503
  format,
31908
- target: failure.target,
34504
+ target: failure2.target,
31909
34505
  matchedFileCount: 0,
31910
34506
  results: [],
31911
- diagnostics: aggregateDiagnostics(failure.diagnostics),
34507
+ diagnostics: aggregateDiagnostics(failure2.diagnostics),
31912
34508
  exitCode: exitCode2
31913
34509
  };
31914
34510
  if (format === "json") {
@@ -31957,8 +34553,8 @@ function handleTest(target, format, runtime) {
31957
34553
  function handleNormalizeOrExplain(command, inputPath, format, runtime) {
31958
34554
  const resolved = resolveSingleFilePath(runtime.cwd, inputPath);
31959
34555
  if (!resolved.success) {
31960
- const failure = resolved;
31961
- const diagnostics = aggregateDiagnostics(failure.diagnostics);
34556
+ const failure2 = resolved;
34557
+ const diagnostics = aggregateDiagnostics(failure2.diagnostics);
31962
34558
  const exitCode2 = determineExitCode(diagnostics) || 1;
31963
34559
  const baseState = createInvalidSingleFileState(
31964
34560
  runtime.cwd,
@@ -32197,7 +34793,7 @@ function isLegacyRulesArgument(value) {
32197
34793
  function isPrettyOrJson(format) {
32198
34794
  return format === "pretty" || format === "json";
32199
34795
  }
32200
- function runCli(args = process.argv.slice(2), runtime = {}) {
34796
+ async function runCli(args = process.argv.slice(2), runtime = {}) {
32201
34797
  const resolvedRuntime = resolveRuntime(runtime);
32202
34798
  if (args.length === 0 || args[0] === "help" || args[0] === "--help") {
32203
34799
  resolvedRuntime.writeStdout(renderHelpMessage());
@@ -32225,7 +34821,7 @@ function runCli(args = process.argv.slice(2), runtime = {}) {
32225
34821
  );
32226
34822
  return 1;
32227
34823
  }
32228
- return handleCheck(
34824
+ return await handleCheck(
32229
34825
  parsed2.positionals[0],
32230
34826
  parsed2.format,
32231
34827
  resolvedRuntime,
@@ -32380,10 +34976,12 @@ function isCliEntrypoint() {
32380
34976
  if (!mainFilename) {
32381
34977
  return false;
32382
34978
  }
32383
- return mainFilename === (0, import_node_path15.resolve)(__dirname, "..", "..", "..", "main.js");
34979
+ return mainFilename === (0, import_node_path22.resolve)(__dirname, "..", "..", "..", "main.js");
32384
34980
  }
32385
34981
  if (isCliEntrypoint()) {
32386
- process.exitCode = runCli();
34982
+ void runCli().then((exitCode) => {
34983
+ process.exitCode = exitCode;
34984
+ });
32387
34985
  }
32388
34986
  // Annotate the CommonJS export names for ESM import in node:
32389
34987
  0 && (module.exports = {