@sema-agent/server 7.79.2 → 7.79.3

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.
@@ -206,7 +206,6 @@ function forgetEscapedSeparatorTails() {
206
206
  escapedTailsValue = undefined;
207
207
  }
208
208
  const VALUE_ARM_STICKY = new Map();
209
- const POOL_ONLY_ARMS = new Set();
210
209
  const VALUE_ARM_BODIES = [];
211
210
  const valueArm = (prefixSrc, valueClass, min, evidence) => {
212
211
  const body = `${prefixSrc}${twoStrongAhead(valueClass)}${atLeast(valueClass, min)}`;
@@ -226,28 +225,31 @@ const valueArm = (prefixSrc, valueClass, min, evidence) => {
226
225
  }
227
226
  }
228
227
  })();
229
- POOL_ONLY_ARMS.add(g);
230
228
  return g;
231
229
  };
232
230
  let anyValueArm;
233
231
  function anyValueArmRe() {
234
232
  return (anyValueArm ??= new RegExp(`(?:${VALUE_ARM_BODIES.join("|")})`, "y"));
235
233
  }
236
- const ARM_RANK = { keyword: 1, shape: 2, fragment: 3 };
234
+ const ARM_RANK = { structural: 0, keyword: 1, shape: 2, fragment: 3 };
237
235
  let poolSpanCount = 0;
238
236
  let poolArmCount = 0;
239
237
  let poolBoundaryArmCount = 0;
240
238
  let coveredPositions = 0;
241
239
  let headScreenHits = 0;
242
240
  let fullYCalls = 0;
241
+ let forensicLevels = 0;
242
+ let ssh2ScanBytes = 0;
243
243
  export function armPoolBudgetForTest() {
244
- return { spanCount: poolSpanCount, coveredPositions, headScreenHits, fullYCalls, armCount: poolArmCount, boundaryArmCount: poolBoundaryArmCount };
244
+ return { spanCount: poolSpanCount, coveredPositions, headScreenHits, fullYCalls, armCount: poolArmCount, boundaryArmCount: poolBoundaryArmCount, forensicLevels, ssh2ScanBytes };
245
245
  }
246
246
  export function resetArmPoolBudgetForTest() {
247
247
  poolSpanCount = 0;
248
248
  coveredPositions = 0;
249
249
  headScreenHits = 0;
250
250
  fullYCalls = 0;
251
+ forensicLevels = 0;
252
+ ssh2ScanBytes = 0;
251
253
  }
252
254
  const unitBit = (t, c) => (t[c >>> 5] >>> (c & 31)) & 1;
253
255
  let codeUnitDomain;
@@ -477,16 +479,17 @@ function deriveLiteralHead(re) {
477
479
  first: unionMembers(per.map((p) => p.first)),
478
480
  };
479
481
  }
480
- function scanResumeAfterHit(start, end, head) {
481
- return Math.max(start + 1, end - Math.max(head - 1, 0));
482
+ function scanResumeAfterHit(start, end, back) {
483
+ return Math.max(start + 1, end - back);
482
484
  }
485
+ const spanOffset = (a, m) => (a.outside === 1 ? m[1].length : 0);
483
486
  function collectRegexSpans(a, text, out) {
484
487
  const g = a.g;
485
488
  g.lastIndex = 0;
486
489
  for (let m = g.exec(text); m !== null; m = g.exec(text)) {
487
490
  const end = m.index + m[0].length;
488
- out.push({ start: m.index, end, repl: a.repl, rank: a.rank, order: a.order });
489
- g.lastIndex = scanResumeAfterHit(m.index, end, a.head);
491
+ out.push({ start: m.index + spanOffset(a, m), end, repl: a.repl, rank: a.rank, order: a.order });
492
+ g.lastIndex = scanResumeAfterHit(m.index, end, a.back);
490
493
  }
491
494
  }
492
495
  function resolveSpanOverlaps(spans) {
@@ -514,7 +517,7 @@ function applySpans(text, spans) {
514
517
  return { out: parts.join(""), count: spans.length };
515
518
  }
516
519
  function collectValueArmSpans(arm, text, out) {
517
- const { g, y, head, repl, rank, order } = arm;
520
+ const { g, y, back, repl, rank, order } = arm;
518
521
  const tails = escapedSeparatorTails(text);
519
522
  if (tails === undefined) {
520
523
  collectRegexSpans(arm, text, out);
@@ -563,11 +566,11 @@ function collectValueArmSpans(arm, text, out) {
563
566
  else
564
567
  break;
565
568
  out.push({ start: idx, end, repl, rank, order });
566
- pos = scanResumeAfterHit(idx, end, head);
569
+ pos = scanResumeAfterHit(idx, end, back);
567
570
  }
568
571
  }
569
572
  function collectStickySpans(a, text, at, out) {
570
- const { y, head, repl, rank, order } = a;
573
+ const { y, back, repl, rank, order } = a;
571
574
  let pos = 0;
572
575
  for (const p of at) {
573
576
  if (p < pos)
@@ -578,15 +581,14 @@ function collectStickySpans(a, text, at, out) {
578
581
  if (m === null)
579
582
  continue;
580
583
  const end = p + m[0].length;
581
- out.push({ start: p, end, repl, rank, order });
582
- pos = scanResumeAfterHit(p, end, head);
584
+ out.push({ start: p + spanOffset(a, m), end, repl, rank, order });
585
+ pos = scanResumeAfterHit(p, end, back);
583
586
  }
584
587
  }
585
588
  const WORD_SEGMENT_CLASS = "[A-Za-z0-9._~+/-]";
586
589
  const keywordArm = (keywordSrc, valueClass, min, tailSrc = "") => {
587
590
  const re = new RegExp(`(?:${keywordSrc}\\s+${atLeast(valueClass, min)}${tailSrc}` +
588
591
  `|(?<!${WORD_SEGMENT_CLASS})${keywordSrc}(?:${twoStrongAhead(valueClass)}|${noConnectorAhead(valueClass)})${atLeast(valueClass, min)}${tailSrc})`, "g");
589
- POOL_ONLY_ARMS.add(re);
590
592
  return re;
591
593
  };
592
594
  const STRUCTURAL_SECRET_PATTERNS = [
@@ -633,6 +635,8 @@ function skipPemSeps(s, i, limit) {
633
635
  for (;;) {
634
636
  if (i >= limit)
635
637
  return i;
638
+ if (opaqueUnitEnd(i) >= 0)
639
+ return i;
636
640
  const c = s.charCodeAt(i);
637
641
  if (isSepCode(c)) {
638
642
  i += 1;
@@ -675,16 +679,16 @@ function pemInlineSepLen(s, i, limit) {
675
679
  return 0;
676
680
  }
677
681
  function probePemHeaderToken(s, p, limit) {
678
- if (p >= limit || !isIdentStartCode(s.charCodeAt(p)))
682
+ if (p >= limit || opaqueUnitEnd(p) >= 0 || !isIdentStartCode(s.charCodeAt(p)))
679
683
  return -1;
680
684
  p += 1;
681
685
  let identLen = 1;
682
- while (identLen < PEM_HEADER_IDENT_MAX && p < limit && isIdentCode(s.charCodeAt(p))) {
686
+ while (identLen < PEM_HEADER_IDENT_MAX && p < limit && opaqueUnitEnd(p) < 0 && isIdentCode(s.charCodeAt(p))) {
683
687
  p += 1;
684
688
  identLen += 1;
685
689
  }
686
690
  for (;;) {
687
- if (p >= limit)
691
+ if (p >= limit || opaqueUnitEnd(p) >= 0)
688
692
  return -1;
689
693
  const c = s.charCodeAt(p);
690
694
  if (c === CH_SPACE || c === CH_TAB) {
@@ -716,6 +720,11 @@ function parsePemHeaderLine(s, i, limit, allowBare, json) {
716
720
  for (;;) {
717
721
  if (p >= limit)
718
722
  return setPemHdr(start, p, p, true);
723
+ const unit = opaqueUnitEnd(p);
724
+ if (unit >= 0) {
725
+ p = Math.min(unit, limit);
726
+ continue;
727
+ }
719
728
  const c = s.charCodeAt(p);
720
729
  if (c !== CH_CR && c !== CH_LF && c !== CH_SPACE && c !== CH_TAB && c !== CH_BACKSLASH && c !== CH_QUOTE) {
721
730
  p += 1;
@@ -741,6 +750,8 @@ function parsePemHeaderLine(s, i, limit, allowBare, json) {
741
750
  }
742
751
  let q = p;
743
752
  for (;;) {
753
+ if (opaqueUnitEnd(q) >= 0)
754
+ break;
744
755
  const b = pemLineBreakLen(s, q, limit);
745
756
  if (b === 1)
746
757
  return setPemHdr(start, p, q + 1, false);
@@ -766,7 +777,7 @@ function parsePemBody(s, i, limit) {
766
777
  for (;;) {
767
778
  const q = skipPemSeps(s, p, limit);
768
779
  let r = q;
769
- while (r < limit && isBase64Code(s.charCodeAt(r)))
780
+ while (r < limit && opaqueUnitEnd(r) < 0 && isBase64Code(s.charCodeAt(r)))
770
781
  r += 1;
771
782
  if (r - q < PEM_BODY_RUN_MIN)
772
783
  break;
@@ -779,13 +790,15 @@ function rightmostPemRunEnd(s, from, to) {
779
790
  let best = -1;
780
791
  let runStart = -1;
781
792
  for (let p = from; p <= to; p++) {
782
- if (p < to && s.charCodeAt(p) === CH_LAQUO) {
783
- const tokenEnd = redactionTokenEndAt(s, p, to);
784
- if (tokenEnd > 0) {
793
+ if (p < to) {
794
+ const unit = opaqueUnitEnd(p);
795
+ if (unit > 0) {
785
796
  if (runStart >= 0 && p - runStart >= PEM_BODY_RUN_MIN)
786
797
  best = p;
787
798
  runStart = -1;
788
- p = tokenEnd - 1;
799
+ if (unit > to)
800
+ return best;
801
+ p = unit - 1;
789
802
  continue;
790
803
  }
791
804
  }
@@ -832,15 +845,74 @@ function findPemFence(text, from) {
832
845
  if (close < 0)
833
846
  return undefined;
834
847
  from = labelFrom;
848
+ if (inOpaqueUnit(fence))
849
+ continue;
835
850
  if (!isPemFenceLabel(text, labelFrom, close))
836
851
  continue;
837
852
  return { fence, after: close + PEM_FENCE_CLOSE.length };
838
853
  }
839
854
  }
840
- function redactPemFragments(text, json) {
841
- let count = 0;
842
- let out;
843
- let copied = 0;
855
+ const SSH2_FENCE_OPEN = "---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----";
856
+ const SSH2_FENCE_CLOSE = "---- END SSH2 ENCRYPTED PRIVATE KEY ----";
857
+ const SSH2_BODY_MAX = 8192;
858
+ function unitCountWithin(from, to, max) {
859
+ if (to - from <= max)
860
+ return true;
861
+ const st = scanEvidence.starts;
862
+ if (st.length === 0)
863
+ return false;
864
+ const k = firstCredentialSpanFrom(scanEvidence, from);
865
+ if (k >= st.length || st[k] >= to)
866
+ return false;
867
+ let n = 0;
868
+ for (let p = from; p < to;) {
869
+ if (n > max)
870
+ return false;
871
+ const unit = opaqueUnitEnd(p);
872
+ p = unit > 0 && unit <= to ? unit : p + 1;
873
+ n += 1;
874
+ }
875
+ return n <= max;
876
+ }
877
+ function collectSsh2FenceSpans(text, span) {
878
+ let cachedFrom = 0;
879
+ let cachedAt;
880
+ const nextClose = (from) => {
881
+ if (cachedAt !== undefined && cachedFrom <= from && (cachedAt < 0 || cachedAt >= from))
882
+ return cachedAt;
883
+ let p = text.indexOf(SSH2_FENCE_CLOSE, from);
884
+ ssh2ScanBytes += (p < 0 ? text.length : p) - from;
885
+ while (p >= 0 && inOpaqueUnit(p)) {
886
+ const q = text.indexOf(SSH2_FENCE_CLOSE, p + 1);
887
+ ssh2ScanBytes += (q < 0 ? text.length : q) - (p + 1);
888
+ p = q;
889
+ }
890
+ cachedFrom = from;
891
+ cachedAt = p;
892
+ return p;
893
+ };
894
+ for (let from = 0;;) {
895
+ const fence = text.indexOf(SSH2_FENCE_OPEN, from);
896
+ if (fence < 0)
897
+ return;
898
+ if (inOpaqueUnit(fence)) {
899
+ from = fence + 1;
900
+ continue;
901
+ }
902
+ const bodyFrom = fence + SSH2_FENCE_OPEN.length;
903
+ const close = nextClose(bodyFrom);
904
+ if (close < 0)
905
+ return;
906
+ if (!unitCountWithin(bodyFrom, close, SSH2_BODY_MAX)) {
907
+ from = fence + 1;
908
+ continue;
909
+ }
910
+ const end = close + SSH2_FENCE_CLOSE.length;
911
+ span(fence, end);
912
+ from = scanResumeAfterHit(fence, end, SSH2_FENCE_OPEN.length);
913
+ }
914
+ }
915
+ function collectPemFragmentSpans(text, json, span) {
844
916
  let cur = findPemFence(text, 0);
845
917
  let inherited = false;
846
918
  while (cur !== undefined) {
@@ -867,29 +939,24 @@ function redactPemFragments(text, json) {
867
939
  let bodyEnd = parsePemBody(text, pos, limit);
868
940
  if (bodyEnd < 0)
869
941
  bodyEnd = rightmost;
870
- if (bodyEnd >= 0) {
871
- out ??= [];
872
- out.push(text.slice(copied, cur.fence), PEM_FRAGMENT_TOKEN);
873
- copied = bodyEnd;
874
- count += 1;
875
- }
942
+ if (bodyEnd >= 0)
943
+ span(cur.fence, bodyEnd);
876
944
  cur = next;
877
945
  }
878
- if (out === undefined)
879
- return { out: text, count: 0 };
880
- out.push(text.slice(copied));
881
- return { out: out.join(""), count };
882
946
  }
883
- function redactHeadlessPpkBodies(text) {
947
+ function collectHeadlessPpkSpans(text, span) {
884
948
  const KEY = "Private-Lines: ";
885
949
  const n = text.length;
886
950
  const isB64 = (c) => (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 43 || c === 47 || c === 61;
887
- const skipHs = (i) => { while (i < n && (text.charCodeAt(i) === 32 || text.charCodeAt(i) === 9))
951
+ const b64At = (i) => opaqueUnitEnd(i) < 0 && isB64(text.charCodeAt(i));
952
+ const skipHs = (i) => { while (i < n && opaqueUnitEnd(i) < 0 && (text.charCodeAt(i) === 32 || text.charCodeAt(i) === 9))
888
953
  i += 1; return i; };
889
954
  const skipNewlines = (i) => {
890
955
  let q = skipHs(i);
891
956
  let seen = false;
892
957
  for (;;) {
958
+ if (opaqueUnitEnd(q) >= 0)
959
+ break;
893
960
  let any = false;
894
961
  if (text.charCodeAt(q) === 13) {
895
962
  q += 1;
@@ -921,6 +988,8 @@ function redactHeadlessPpkBodies(text) {
921
988
  return seen ? q : -1;
922
989
  };
923
990
  const escapedNewlineLen = (i) => {
991
+ if (opaqueUnitEnd(i) >= 0)
992
+ return 0;
924
993
  let k = i;
925
994
  while (k < n && text.charCodeAt(k) === 92)
926
995
  k += 1;
@@ -933,20 +1002,23 @@ function redactHeadlessPpkBodies(text) {
933
1002
  const b = skipHs(i);
934
1003
  if (b >= n)
935
1004
  return true;
1005
+ if (opaqueUnitEnd(b) >= 0)
1006
+ return false;
936
1007
  const c = text.charCodeAt(b);
937
1008
  return c === 10 || c === 13 || c === 34 || escapedNewlineLen(b) > 0;
938
1009
  };
939
- let out = "";
940
- let last = 0;
941
- let count = 0;
942
1010
  let search = 0;
943
1011
  for (;;) {
944
1012
  const at = text.indexOf(KEY, search);
945
1013
  if (at === -1)
946
1014
  break;
1015
+ if (inOpaqueUnit(at)) {
1016
+ search = at + KEY.length;
1017
+ continue;
1018
+ }
947
1019
  let p = at + KEY.length;
948
1020
  const d0 = p;
949
- while (p < n && text.charCodeAt(p) >= 48 && text.charCodeAt(p) <= 57)
1021
+ while (p < n && opaqueUnitEnd(p) < 0 && text.charCodeAt(p) >= 48 && text.charCodeAt(p) <= 57)
950
1022
  p += 1;
951
1023
  if (p === d0) {
952
1024
  search = at + KEY.length;
@@ -959,15 +1031,15 @@ function redactHeadlessPpkBodies(text) {
959
1031
  if (q0 === -1)
960
1032
  break;
961
1033
  let q = q0;
962
- const mk = text.charCodeAt(q);
1034
+ const mk = opaqueUnitEnd(q) < 0 ? text.charCodeAt(q) : -1;
963
1035
  if (mk === 43 || mk === 45)
964
1036
  q = skipHs(q + 1);
965
1037
  let r = q;
966
- while (r < n && isB64(text.charCodeAt(r)))
1038
+ while (r < n && b64At(r))
967
1039
  r += 1;
968
1040
  if (r - q < 4) {
969
1041
  r = q0;
970
- while (r < n && isB64(text.charCodeAt(r)))
1042
+ while (r < n && b64At(r))
971
1043
  r += 1;
972
1044
  if (r - q0 < 4)
973
1045
  break;
@@ -981,12 +1053,9 @@ function redactHeadlessPpkBodies(text) {
981
1053
  search = p;
982
1054
  continue;
983
1055
  }
984
- out += text.slice(last, at) + HEADLESS_PPK_TOKEN;
985
- last = end;
1056
+ span(at, end);
986
1057
  search = end;
987
- count += 1;
988
1058
  }
989
- return count === 0 ? { out: text, count: 0 } : { out: out + text.slice(last), count };
990
1059
  }
991
1060
  const FRAGMENT_SECRET_PATTERNS = [
992
1061
  [valueArm("sk-", "[A-Za-z0-9_-]", 6, "needs-left-boundary"), "«redacted:key:partial»"],
@@ -1000,21 +1069,12 @@ const FRAGMENT_SECRET_PATTERNS = [
1000
1069
  ];
1001
1070
  const REDACTION_TOKENS = new Set([...FULL_ARM_PATTERNS.map(([, r]) => r), PEM_FRAGMENT_TOKEN, ...FRAGMENT_SECRET_PATTERNS.map(([, r]) => r)]
1002
1071
  .flatMap((r) => r.match(/«[^»]*»/g) ?? []));
1003
- const REDACTION_TOKEN_MAX = Math.max(...[...REDACTION_TOKENS].map((t) => t.length));
1004
1072
  {
1005
1073
  const closed = new RegExp(`^${REDACTION_TOKEN_RE_SRC}$`);
1006
1074
  for (const t of REDACTION_TOKENS)
1007
1075
  if (!closed.test(t))
1008
1076
  throw new Error(`redact: token ${t} is outside the closed form ${REDACTION_TOKEN_RE_SRC}`);
1009
1077
  }
1010
- function redactionTokenEndAt(s, p, limit) {
1011
- const max = Math.min(limit, p + REDACTION_TOKEN_MAX);
1012
- for (let e = p + 2; e <= max; e++) {
1013
- if (s.charCodeAt(e - 1) === 187)
1014
- return REDACTION_TOKENS.has(s.slice(p, e)) ? e : 0;
1015
- }
1016
- return 0;
1017
- }
1018
1078
  const labelOfToken = (token) => token.replace(/[«»]/g, "").replace(/^redacted:/, "");
1019
1079
  const FRAGMENT_ARM_TOKENS = new Set(FRAGMENT_SECRET_PATTERNS.map(([, repl]) => repl));
1020
1080
  {
@@ -1031,42 +1091,118 @@ const FRAGMENT_ARM_TOKENS = new Set(FRAGMENT_SECRET_PATTERNS.map(([, repl]) => r
1031
1091
  }
1032
1092
  const armOfToken = (repl) => (FRAGMENT_ARM_TOKENS.has(repl) ? "fragment" : "full");
1033
1093
  const NO_HITS = [];
1034
- const singleHit = (label, arm, r) => r.count > 0 ? { out: r.out, hits: [{ label, count: r.count, arm }] } : { out: r.out, hits: NO_HITS };
1035
- const regexRule = ([re, repl]) => {
1036
- const groupCount = (new RegExp(`${re.source}|`).exec("")?.length ?? 1) - 1;
1037
- const refs = [...repl.matchAll(/\$(.)/g)].map(([, d]) => d ?? "");
1038
- for (const d of refs) {
1039
- if (!/^[1-9]$/.test(d) || Number(d) > groupCount)
1040
- throw new Error(`redact: replacement ${JSON.stringify(repl)} uses unsupported ref $${d}`);
1041
- }
1042
- const expand = refs.length === 0 ? undefined : (args) => repl.replace(/\$([1-9])/g, (_, d) => args[Number(d)] ?? "");
1043
- const arm = armOfToken(repl);
1044
- if (POOL_ONLY_ARMS.has(re) || arm === "fragment")
1045
- throw new Error(`redact: rule ${JSON.stringify(repl)} belongs in the arm pool, not a per-arm section`);
1046
- const label = labelOfToken(repl);
1047
- const applyOnce = (text, wantCount) => {
1048
- if (!wantCount)
1049
- return { out: text.replace(re, repl), count: 0 };
1050
- let n = 0;
1051
- const out = text.replace(re, (...args) => {
1052
- n += 1;
1053
- return expand === undefined ? repl : expand(args);
1054
- });
1055
- return { out, count: n };
1056
- };
1057
- return { run: (text, wantCount) => singleHit(label, arm, applyOnce(text, wantCount)) };
1058
- };
1059
1094
  const URL_USERINFO_PATTERN = (() => {
1060
1095
  const row = FULL_ARM_PATTERNS.find(([, repl]) => repl.endsWith("»@"));
1061
1096
  if (row === undefined)
1062
1097
  throw new Error("redact: URL-userinfo rule missing from FULL_ARM_PATTERNS");
1063
1098
  return row;
1064
1099
  })();
1100
+ const SSH2_FENCE_PATTERN = (() => {
1101
+ const row = FULL_ARM_PATTERNS.find(([re]) => re.source.startsWith(SSH2_FENCE_OPEN));
1102
+ if (row === undefined)
1103
+ throw new Error("redact: SSH2 fence rule missing from FULL_ARM_PATTERNS");
1104
+ return row;
1105
+ })();
1065
1106
  const isRegexSpace = (c) => c === 32 || (c >= 9 && c <= 13) || c === 0xa0 || c === 0x1680 || (c >= 0x2000 && c <= 0x200a) || c === 0x2028 || c === 0x2029 || c === 0x202f || c === 0x205f || c === 0x3000 || c === 0xfeff;
1066
1107
  const isLineTerminator = (c) => c === 10 || c === 13 || c === 0x2028 || c === 0x2029;
1108
+ const TOKEN_UNIT_Y = new RegExp(REDACTION_TOKEN_RE_SRC, "y");
1109
+ function tokenUnitEnd(text, p) {
1110
+ TOKEN_UNIT_Y.lastIndex = p;
1111
+ return TOKEN_UNIT_Y.test(text) ? TOKEN_UNIT_Y.lastIndex : -1;
1112
+ }
1113
+ const NO_CREDENTIAL_SPANS = { starts: new Int32Array(0), ends: new Int32Array(0) };
1114
+ function credentialSpans(text, spans, withSpans) {
1115
+ const iv = withSpans ? spans.map((sp) => [sp.start, sp.end]) : [];
1116
+ for (let i = text.indexOf("«"); i >= 0; i = text.indexOf("«", i + 1)) {
1117
+ const e = tokenUnitEnd(text, i);
1118
+ if (e > 0) {
1119
+ iv.push([i, e]);
1120
+ i = e - 1;
1121
+ }
1122
+ }
1123
+ if (iv.length === 0)
1124
+ return NO_CREDENTIAL_SPANS;
1125
+ iv.sort((a, b) => a[0] - b[0]);
1126
+ const starts = new Int32Array(iv.length);
1127
+ const ends = new Int32Array(iv.length);
1128
+ let k = 0;
1129
+ let [cs, ce] = iv[0];
1130
+ for (let j = 1; j < iv.length; j++) {
1131
+ const [s2, e2] = iv[j];
1132
+ if (s2 <= ce)
1133
+ ce = Math.max(ce, e2);
1134
+ else {
1135
+ starts[k] = cs;
1136
+ ends[k] = ce;
1137
+ k += 1;
1138
+ cs = s2;
1139
+ ce = e2;
1140
+ }
1141
+ }
1142
+ starts[k] = cs;
1143
+ ends[k] = ce;
1144
+ k += 1;
1145
+ return { starts: starts.subarray(0, k), ends: ends.subarray(0, k) };
1146
+ }
1147
+ function sameCredentialSpans(a, b) {
1148
+ if (a.starts.length !== b.starts.length)
1149
+ return false;
1150
+ for (let i = 0; i < a.starts.length; i++)
1151
+ if (a.starts[i] !== b.starts[i] || a.ends[i] !== b.ends[i])
1152
+ return false;
1153
+ return true;
1154
+ }
1155
+ function firstCredentialSpanFrom(cs, from) {
1156
+ let lo = 0;
1157
+ let hi = cs.starts.length;
1158
+ while (lo < hi) {
1159
+ const mid = (lo + hi) >>> 1;
1160
+ if (cs.starts[mid] < from)
1161
+ lo = mid + 1;
1162
+ else
1163
+ hi = mid;
1164
+ }
1165
+ return lo;
1166
+ }
1167
+ function credentialSpansCover(cs, from, to) {
1168
+ let k = firstCredentialSpanFrom(cs, from + 1) - 1;
1169
+ if (k < 0)
1170
+ k = 0;
1171
+ return k < cs.starts.length && cs.starts[k] <= from && cs.ends[k] >= to;
1172
+ }
1173
+ let scanEvidence = NO_CREDENTIAL_SPANS;
1174
+ let scanEvidenceCursor = 0;
1175
+ function setScanEvidence(cs) {
1176
+ scanEvidence = cs;
1177
+ scanEvidenceCursor = 0;
1178
+ }
1179
+ function inOpaqueUnit(p) {
1180
+ const st = scanEvidence.starts;
1181
+ if (st.length === 0)
1182
+ return false;
1183
+ const k = firstCredentialSpanFrom(scanEvidence, p) - 1;
1184
+ return k >= 0 && st[k] < p && scanEvidence.ends[k] > p;
1185
+ }
1186
+ function opaqueUnitEnd(p) {
1187
+ const st = scanEvidence.starts;
1188
+ if (st.length === 0)
1189
+ return -1;
1190
+ let k = scanEvidenceCursor;
1191
+ while (k < st.length && st[k] < p)
1192
+ k += 1;
1193
+ while (k > 0 && st[k - 1] >= p)
1194
+ k -= 1;
1195
+ scanEvidenceCursor = k;
1196
+ return k < st.length && st[k] === p ? scanEvidence.ends[k] : -1;
1197
+ }
1067
1198
  function scanUserinfoRun(text, i, kind, json) {
1068
1199
  const n = text.length;
1069
1200
  while (i < n) {
1201
+ const unit = opaqueUnitEnd(i);
1202
+ if (unit >= 0) {
1203
+ i = unit;
1204
+ continue;
1205
+ }
1070
1206
  const c = text.charCodeAt(i);
1071
1207
  if (json && c === 92) {
1072
1208
  if (i + 1 < n && !isLineTerminator(text.charCodeAt(i + 1))) {
@@ -1095,48 +1231,19 @@ function scanUserinfoRun(text, i, kind, json) {
1095
1231
  }
1096
1232
  return i;
1097
1233
  }
1098
- const REDACTION_TOKEN_Y = new RegExp(REDACTION_TOKEN_RE_SRC, "y");
1099
- const SHAPE_STICKY = SHAPE_SECRET_PATTERNS.map(([re]) => new RegExp(re.source, "y"));
1100
- function keyShapedUsernameEnd(text, p) {
1101
- for (const re of SHAPE_STICKY) {
1102
- re.lastIndex = p;
1103
- if (re.test(text))
1104
- return re.lastIndex;
1105
- }
1106
- return -1;
1107
- }
1108
1234
  function matchUserinfoAt(text, p, json) {
1109
- REDACTION_TOKEN_Y.lastIndex = p;
1110
- const tokEnd = REDACTION_TOKEN_Y.test(text) ? REDACTION_TOKEN_Y.lastIndex : -1;
1111
- const afterTok = tokEnd >= 0 ? text.charCodeAt(tokEnd) : -1;
1112
- if (afterTok === 64)
1235
+ const userEnd = scanUserinfoRun(text, p, "plain-user", json);
1236
+ if (text.charCodeAt(userEnd) !== 58)
1113
1237
  return null;
1114
- let userEnd = -1;
1115
- let kind;
1116
- if (afterTok === 58) {
1117
- userEnd = tokEnd;
1118
- kind = "token-pass";
1119
- }
1120
- else if ((() => { const e = keyShapedUsernameEnd(text, p); return e >= 0 && text.charCodeAt(e) === 58 ? ((userEnd = e), true) : false; })()) {
1121
- kind = "token-pass";
1122
- }
1123
- else {
1124
- userEnd = scanUserinfoRun(text, p, "plain-user", json);
1125
- if (text.charCodeAt(userEnd) !== 58)
1126
- return null;
1127
- kind = "plain-pass";
1128
- }
1129
- const passEnd = scanUserinfoRun(text, userEnd + 1, kind, json);
1238
+ const keyId = userEnd > p && credentialSpansCover(scanEvidence, p, userEnd);
1239
+ const passEnd = scanUserinfoRun(text, userEnd + 1, keyId ? "token-pass" : "plain-pass", json);
1130
1240
  if (passEnd === userEnd + 1 || text.charCodeAt(passEnd) !== 64)
1131
1241
  return null;
1132
1242
  return [userEnd, passEnd + 1];
1133
1243
  }
1134
- function redactUrlUserinfo(text, json) {
1135
- const [head, repl] = URL_USERINFO_PATTERN;
1244
+ function collectUrlUserinfoSpans(text, json, span) {
1245
+ const head = URL_USERINFO_PATTERN[0];
1136
1246
  head.lastIndex = 0;
1137
- let out = "";
1138
- let last = 0;
1139
- let count = 0;
1140
1247
  for (let m = head.exec(text); m !== null; m = head.exec(text)) {
1141
1248
  const colon = m.index;
1142
1249
  const hit = matchUserinfoAt(text, colon + 3, json);
@@ -1144,18 +1251,14 @@ function redactUrlUserinfo(text, json) {
1144
1251
  head.lastIndex = colon + 1;
1145
1252
  continue;
1146
1253
  }
1147
- const user = text.slice(colon, hit[0]);
1148
- out += text.slice(last, colon) + repl.replace(/\$1/g, () => user);
1149
- last = hit[1];
1254
+ span(hit[0], hit[1]);
1150
1255
  head.lastIndex = hit[1];
1151
- count += 1;
1152
1256
  }
1153
- return count === 0 ? { out: text, count: 0 } : { out: out + text.slice(last), count };
1154
1257
  }
1155
1258
  let ppkScanBytes = 0;
1156
1259
  export function ppkScanBytesForTest() { return ppkScanBytes; }
1157
1260
  export function resetPpkScanBytesForTest() { ppkScanBytes = 0; }
1158
- function redactPpkContainers(text, record) {
1261
+ function collectPpkContainerSpans(text, record, span) {
1159
1262
  const HEAD = "PuTTY-User-Key-File-";
1160
1263
  const MAC = "Private-MAC: ";
1161
1264
  const HASH = "Private-Hash: ";
@@ -1174,7 +1277,7 @@ function redactPpkContainers(text, record) {
1174
1277
  const isVersionedHead = (at) => {
1175
1278
  let p = at + HEAD.length;
1176
1279
  const start = p;
1177
- while (p < text.length) {
1280
+ while (p < text.length && opaqueUnitEnd(p) < 0) {
1178
1281
  const c = text.charCodeAt(p);
1179
1282
  if (c >= 48 && c <= 57)
1180
1283
  p += 1;
@@ -1185,13 +1288,13 @@ function redactPpkContainers(text, record) {
1185
1288
  };
1186
1289
  const nextVersionedHead = (from) => {
1187
1290
  let p = find(HEAD, from);
1188
- while (p !== Number.POSITIVE_INFINITY && !isVersionedHead(p))
1291
+ while (p !== Number.POSITIVE_INFINITY && (inOpaqueUnit(p) || !isVersionedHead(p)))
1189
1292
  p = find(HEAD, p + HEAD.length);
1190
1293
  return p;
1191
1294
  };
1192
1295
  const hexRunEnd = (from) => {
1193
1296
  let e = from;
1194
- while (e < text.length) {
1297
+ while (e < text.length && opaqueUnitEnd(e) < 0) {
1195
1298
  const c = text.charCodeAt(e);
1196
1299
  if ((c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70))
1197
1300
  e += 1;
@@ -1200,9 +1303,6 @@ function redactPpkContainers(text, record) {
1200
1303
  }
1201
1304
  return e;
1202
1305
  };
1203
- let out = "";
1204
- let last = 0;
1205
- let count = 0;
1206
1306
  let at = nextVersionedHead(0);
1207
1307
  while (at !== Number.POSITIVE_INFINITY) {
1208
1308
  let pos = at + HEAD.length + 1;
@@ -1229,6 +1329,10 @@ function redactPpkContainers(text, record) {
1229
1329
  let gapBlank = true;
1230
1330
  if (limit !== Number.POSITIVE_INFINITY) {
1231
1331
  for (let i = hexEnd; i < limit; i++) {
1332
+ if (opaqueUnitEnd(i) >= 0) {
1333
+ gapBlank = false;
1334
+ break;
1335
+ }
1232
1336
  const c = text.charCodeAt(i);
1233
1337
  if (c !== 32 && c !== 10 && c !== 13 && c !== 9) {
1234
1338
  gapBlank = false;
@@ -1242,120 +1346,206 @@ function redactPpkContainers(text, record) {
1242
1346
  }
1243
1347
  if (ateToEnd && record)
1244
1348
  recordFailOpen("server.redact.ppk-eat-to-end", `from=${at} len=${text.length}`);
1245
- out += text.slice(last, at) + PPK_CONTAINER_TOKEN;
1246
- last = end;
1247
- count += 1;
1349
+ span(at, end);
1248
1350
  at = end >= text.length ? Number.POSITIVE_INFINITY : nextVersionedHead(end);
1249
1351
  }
1250
- return count === 0 ? { out: text, count: 0 } : { out: out + text.slice(last), count };
1251
1352
  }
1252
- const URL_USERINFO_LABEL = labelOfToken(URL_USERINFO_PATTERN[1]);
1253
- const urlUserinfoRule = { run: (text, _wantCount, json) => singleHit(URL_USERINFO_LABEL, "full", redactUrlUserinfo(text, json)) };
1254
- const armPoolRule = (() => {
1255
- const arms = [
1256
- ...KEYWORD_SECRET_PATTERNS.map((row) => [row, ARM_RANK.keyword]),
1257
- ...SHAPE_SECRET_PATTERNS.map((row) => [row, ARM_RANK.shape]),
1258
- ...FRAGMENT_SECRET_PATTERNS.map((row) => [row, ARM_RANK.fragment]),
1259
- ].map(([[re, repl], rank], order) => {
1260
- if (/\$./.test(repl))
1261
- throw new Error(`redact: arm-pool replacement ${JSON.stringify(repl)} must not use $N refs`);
1353
+ const armPool = (() => {
1354
+ const spanRepl = (token) => {
1355
+ const body = token.startsWith("$1") ? token.slice(2) : token;
1356
+ if (/\$./.test(body))
1357
+ throw new Error(`redact: arm-pool replacement ${JSON.stringify(token)} may only use a leading $1 (the run kept outside the span)`);
1358
+ return body;
1359
+ };
1360
+ const literalAnchorHead = (re) => {
1361
+ const src = re.source;
1362
+ let i = src.startsWith("(") && !src.startsWith("(?") ? 1 : 0;
1363
+ let out = "";
1364
+ for (; i < src.length; i++) {
1365
+ const c = src[i];
1366
+ if (c === "\\") {
1367
+ const e = src[i + 1];
1368
+ if (e === undefined || !HEAD_LITERAL_ESCAPES.has(e))
1369
+ break;
1370
+ if ("*+?{".includes(src[i + 2] ?? ""))
1371
+ break;
1372
+ out += src.slice(i, i + 2);
1373
+ i += 1;
1374
+ continue;
1375
+ }
1376
+ if ("([{|).^$*+?".includes(c))
1377
+ break;
1378
+ if ("*+?{".includes(src[i + 1] ?? ""))
1379
+ break;
1380
+ out += c;
1381
+ }
1382
+ if (out.length === 0)
1383
+ throw new Error(`redact: structural arm /${src}/ has no literal anchor head — its same-arm rewind amount cannot be derived`);
1384
+ return out;
1385
+ };
1386
+ const regexMember = ([re, token], rank, order, structural) => {
1387
+ const repl = spanRepl(token);
1388
+ const outside = token.startsWith("$1") ? 1 : 0;
1262
1389
  const y = VALUE_ARM_STICKY.get(re);
1263
- const head = deriveLiteralHead(re);
1264
- if (y !== undefined && (head.len === 0 || head.src === undefined || head.first === undefined)) {
1265
- throw new Error(`redact: boundary arm /${re.source}/ has a zero-length structural head — the bounded head screen would admit every candidate position`);
1390
+ let back;
1391
+ let headSrc;
1392
+ let headFirst;
1393
+ if (structural) {
1394
+ back = literalAnchorHead(re).length;
1395
+ headSrc = undefined;
1396
+ headFirst = undefined;
1266
1397
  }
1267
- return { g: re, y, head: head.len, headSrc: head.src, headFirst: head.first, repl, rank, order };
1268
- });
1269
- const boundaryArms = arms.flatMap((a) => (a.y === undefined || a.headSrc === undefined || a.headFirst === undefined ? [] : [{ ...a, y: a.y, headSrc: a.headSrc, headFirst: a.headFirst }]));
1270
- const plainArms = arms.filter((a) => a.y === undefined);
1271
- const headScreen = new RegExp(`(?:${boundaryArms.map((a) => a.headSrc).join("|")})`, "y");
1272
- const headFirstUnit = unionMembers(boundaryArms.map((a) => a.headFirst)) ?? new Uint32Array(0x10000 >>> 5).fill(0xffffffff);
1273
- poolArmCount = arms.length;
1274
- poolBoundaryArmCount = boundaryArms.length;
1275
- return {
1276
- run: (text, wantCount) => {
1277
- const spans = [];
1278
- for (const a of plainArms)
1279
- collectRegexSpans(a, text, spans);
1280
- for (const a of boundaryArms)
1281
- collectValueArmSpans(a, text, spans);
1282
- if (boundaryArms.length > 0 && spans.length > 0) {
1283
- const tried = new Uint8Array(text.length + 1);
1284
- const freshCovered = (from) => {
1285
- const at = [];
1286
- for (let k = from; k < spans.length; k++) {
1287
- const s = spans[k];
1288
- for (let p = s.start; p <= s.end; p++) {
1289
- if (tried[p] === 1)
1290
- continue;
1291
- tried[p] = 1;
1292
- coveredPositions += 1;
1293
- if (unitBit(headFirstUnit, text.charCodeAt(p)) !== 1)
1294
- continue;
1295
- headScreen.lastIndex = p;
1296
- if (headScreen.exec(text) === null)
1297
- continue;
1298
- at.push(p);
1299
- }
1300
- }
1301
- headScreenHits += at.length;
1302
- return at.sort((x, y) => x - y);
1303
- };
1304
- for (let at = freshCovered(0); at.length > 0;) {
1305
- const before = spans.length;
1306
- for (const a of boundaryArms)
1307
- collectStickySpans(a, text, at, spans);
1308
- at = freshCovered(before);
1309
- }
1398
+ else {
1399
+ const d = deriveLiteralHead(re);
1400
+ back = Math.max(d.len - 1, 0);
1401
+ headSrc = d.src;
1402
+ headFirst = d.first;
1403
+ if (y !== undefined && (d.len === 0 || headSrc === undefined || headFirst === undefined)) {
1404
+ throw new Error(`redact: boundary arm /${re.source}/ has a zero-length structural head — the bounded head screen would admit every candidate position`);
1310
1405
  }
1311
- poolSpanCount += spans.length;
1312
- const kept = resolveSpanOverlaps(spans);
1313
- const out = applySpans(text, kept).out;
1314
- if (!wantCount || kept.length === 0)
1315
- return { out, hits: NO_HITS };
1316
- const strong = spans.filter((sp) => armOfToken(sp.repl) === "full").sort((a, b) => a.start - b.start);
1317
- const byArmedToken = new Map();
1318
- let si = 0;
1319
- let strongMaxEnd = -1;
1320
- for (const sp of kept) {
1321
- while (si < strong.length && strong[si].start < sp.end) {
1322
- strongMaxEnd = Math.max(strongMaxEnd, strong[si].end);
1323
- si += 1;
1406
+ }
1407
+ if (y !== undefined && outside === 1)
1408
+ throw new Error(`redact: value arm /${re.source}/ cannot carry an outside-span $1 prefix`);
1409
+ const arm = { g: re, y, back, headSrc, headFirst, repl, rank, order, outside };
1410
+ const boundary = y === undefined || headSrc === undefined || headFirst === undefined ? undefined : { ...arm, y, headSrc, headFirst };
1411
+ return {
1412
+ token,
1413
+ rank,
1414
+ order,
1415
+ phase: 1,
1416
+ collect: boundary === undefined
1417
+ ? (text, _json, _observe, out) => collectRegexSpans(arm, text, out)
1418
+ : (text, _json, _observe, out) => collectValueArmSpans(boundary, text, out),
1419
+ retry: boundary === undefined ? undefined : (text, at, out) => collectStickySpans(boundary, text, at, out),
1420
+ headSrc: boundary === undefined ? undefined : headSrc,
1421
+ headFirst: boundary === undefined ? undefined : headFirst,
1422
+ };
1423
+ };
1424
+ const scannerMember = (token, order, phase, scan) => {
1425
+ const repl = spanRepl(token);
1426
+ return {
1427
+ token,
1428
+ rank: ARM_RANK.structural,
1429
+ order,
1430
+ phase,
1431
+ collect: (text, json, observe, out) => scan(text, json, observe, (start, end) => out.push({ start, end, repl, rank: ARM_RANK.structural, order })),
1432
+ retry: undefined,
1433
+ headSrc: undefined,
1434
+ headFirst: undefined,
1435
+ };
1436
+ };
1437
+ const members = [];
1438
+ for (const row of STRUCTURAL_SECRET_PATTERNS) {
1439
+ const order = members.length;
1440
+ members.push(row === URL_USERINFO_PATTERN
1441
+ ? scannerMember(row[1], order, 2, (text, json, _observe, span) => collectUrlUserinfoSpans(text, json, span))
1442
+ : row === SSH2_FENCE_PATTERN
1443
+ ? scannerMember(row[1], order, 2, (text, _json, _observe, span) => collectSsh2FenceSpans(text, span))
1444
+ : regexMember(row, ARM_RANK.structural, order, true));
1445
+ }
1446
+ members.push(scannerMember(PPK_CONTAINER_TOKEN, members.length, 2, (text, _json, observe, span) => collectPpkContainerSpans(text, observe, span)));
1447
+ members.push(scannerMember(PEM_FRAGMENT_TOKEN, members.length, 2, (text, json, _observe, span) => collectPemFragmentSpans(text, json, span)));
1448
+ members.push(scannerMember(HEADLESS_PPK_TOKEN, members.length, 2, (text, _json, _observe, span) => collectHeadlessPpkSpans(text, span)));
1449
+ for (const row of KEYWORD_SECRET_PATTERNS)
1450
+ members.push(regexMember(row, ARM_RANK.keyword, members.length, false));
1451
+ for (const row of SHAPE_SECRET_PATTERNS)
1452
+ members.push(regexMember(row, ARM_RANK.shape, members.length, false));
1453
+ for (const row of FRAGMENT_SECRET_PATTERNS)
1454
+ members.push(regexMember(row, ARM_RANK.fragment, members.length, false));
1455
+ const retryMembers = members.filter((m) => m.retry !== undefined);
1456
+ const phase1 = members.filter((m) => m.phase === 1);
1457
+ const phase2 = members.filter((m) => m.phase === 2);
1458
+ const headScreen = new RegExp(`(?:${retryMembers.map((m) => m.headSrc).join("|")})`, "y");
1459
+ const headFirstUnit = unionMembers(retryMembers.map((m) => m.headFirst)) ?? new Uint32Array(0x10000 >>> 5).fill(0xffffffff);
1460
+ poolArmCount = members.length;
1461
+ poolBoundaryArmCount = retryMembers.length;
1462
+ return (text, wantCount, json, observe) => {
1463
+ const spans = [];
1464
+ let tried;
1465
+ const freshCovered = (from) => {
1466
+ const at = [];
1467
+ const seen = (tried ??= new Uint8Array(text.length + 1));
1468
+ for (let k = from; k < spans.length; k++) {
1469
+ const s = spans[k];
1470
+ for (let p = s.start; p <= s.end; p++) {
1471
+ if (seen[p] === 1)
1472
+ continue;
1473
+ seen[p] = 1;
1474
+ coveredPositions += 1;
1475
+ if (unitBit(headFirstUnit, text.charCodeAt(p)) !== 1)
1476
+ continue;
1477
+ headScreen.lastIndex = p;
1478
+ if (headScreen.exec(text) === null)
1479
+ continue;
1480
+ at.push(p);
1324
1481
  }
1325
- const arm = strongMaxEnd > sp.start ? "full" : armOfToken(sp.repl);
1326
- const key = `${sp.repl}\u0000${arm}`;
1327
- const cur = byArmedToken.get(key);
1328
- if (cur === undefined)
1329
- byArmedToken.set(key, { token: sp.repl, arm, count: 1 });
1330
- else
1331
- cur.count += 1;
1332
1482
  }
1333
- return { out, hits: [...byArmedToken.values()].map(({ token, arm, count }) => ({ label: labelOfToken(token), count, arm })) };
1334
- },
1483
+ headScreenHits += at.length;
1484
+ return at.sort((x, y) => x - y);
1485
+ };
1486
+ const closeCoveragePositions = (from) => {
1487
+ if (retryMembers.length === 0 || spans.length === from)
1488
+ return;
1489
+ for (let at = freshCovered(from); at.length > 0;) {
1490
+ const before = spans.length;
1491
+ for (const m of retryMembers)
1492
+ m.retry(text, at, spans);
1493
+ at = freshCovered(before);
1494
+ }
1495
+ };
1496
+ setScanEvidence(NO_CREDENTIAL_SPANS);
1497
+ for (const m of phase1)
1498
+ m.collect(text, json, observe, spans);
1499
+ closeCoveragePositions(0);
1500
+ if (phase2.length > 0) {
1501
+ let evidence = credentialSpans(text, spans, false);
1502
+ for (let level = 0;; level += 1) {
1503
+ const before = spans.length;
1504
+ setScanEvidence(evidence);
1505
+ for (const m of phase2)
1506
+ m.collect(text, json, observe && level === 0, spans);
1507
+ closeCoveragePositions(before);
1508
+ forensicLevels += 1;
1509
+ const next = credentialSpans(text, spans, true);
1510
+ if (sameCredentialSpans(next, evidence))
1511
+ break;
1512
+ evidence = next;
1513
+ }
1514
+ setScanEvidence(NO_CREDENTIAL_SPANS);
1515
+ }
1516
+ poolSpanCount += spans.length;
1517
+ const kept = resolveSpanOverlaps(spans);
1518
+ const out = applySpans(text, kept).out;
1519
+ if (!wantCount || kept.length === 0)
1520
+ return { out, hits: NO_HITS };
1521
+ const tokenOf = (sp) => members[sp.order].token;
1522
+ const strong = spans.filter((sp) => armOfToken(tokenOf(sp)) === "full").sort((a, b) => a.start - b.start);
1523
+ const byArmedToken = new Map();
1524
+ let si = 0;
1525
+ let strongMaxEnd = -1;
1526
+ for (const sp of kept) {
1527
+ while (si < strong.length && strong[si].start < sp.end) {
1528
+ strongMaxEnd = Math.max(strongMaxEnd, strong[si].end);
1529
+ si += 1;
1530
+ }
1531
+ const token = tokenOf(sp);
1532
+ const arm = strongMaxEnd > sp.start ? "full" : armOfToken(token);
1533
+ const key = `${token}\u0000${arm}`;
1534
+ const cur = byArmedToken.get(key);
1535
+ if (cur === undefined)
1536
+ byArmedToken.set(key, { token, arm, count: 1 });
1537
+ else
1538
+ cur.count += 1;
1539
+ }
1540
+ return { out, hits: [...byArmedToken.values()].map(({ token, arm, count }) => ({ label: labelOfToken(token), count, arm })) };
1335
1541
  };
1336
1542
  })();
1337
- const SECRET_RULES = [
1338
- ...STRUCTURAL_SECRET_PATTERNS.map((p) => (p === URL_USERINFO_PATTERN ? urlUserinfoRule : regexRule(p))),
1339
- { run: (text, _wantCount, _json, observe) => singleHit(labelOfToken(PPK_CONTAINER_TOKEN), armOfToken(PPK_CONTAINER_TOKEN), redactPpkContainers(text, observe)) },
1340
- { run: (text, _wantCount, json) => singleHit(labelOfToken(PEM_FRAGMENT_TOKEN), armOfToken(PEM_FRAGMENT_TOKEN), redactPemFragments(text, json)) },
1341
- { run: (text) => singleHit(labelOfToken(HEADLESS_PPK_TOKEN), armOfToken(HEADLESS_PPK_TOKEN), redactHeadlessPpkBodies(text)) },
1342
- armPoolRule,
1343
- { run: (text, _wantCount, json) => ({ out: redactUrlUserinfo(text, json).out, hits: NO_HITS }) },
1344
- ];
1345
1543
  let onRedaction;
1346
1544
  export function setRedactionObserver(fn) {
1347
1545
  onRedaction = fn;
1348
1546
  }
1349
1547
  function applyRules(text, wantCount, json, observe) {
1350
- let out = text;
1351
- const hits = [];
1352
- for (const rule of SECRET_RULES) {
1353
- const r = rule.run(out, wantCount, json, observe);
1354
- out = r.out;
1355
- if (wantCount && r.hits.length > 0)
1356
- hits.push(...r.hits);
1357
- }
1358
- return { out, hits };
1548
+ return armPool(text, wantCount, json, observe);
1359
1549
  }
1360
1550
  function reportHits(hits) {
1361
1551
  if (onRedaction === undefined)