@sema-agent/server 7.79.2 → 7.80.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/dist/boot/late-binds.d.ts +35 -0
- package/dist/boot/late-binds.js +20 -0
- package/dist/boot/stage-01-config.d.ts +7 -0
- package/dist/boot/stage-01-config.js +60 -0
- package/dist/boot/stage-02-registry-pull.d.ts +10 -0
- package/dist/boot/stage-02-registry-pull.js +11 -0
- package/dist/boot/stage-03-store-backend.d.ts +32 -0
- package/dist/boot/stage-03-store-backend.js +11 -0
- package/dist/boot/stage-04-stores.d.ts +69 -0
- package/dist/boot/stage-04-stores.js +49 -0
- package/dist/boot/stage-05-execution-env.d.ts +82 -0
- package/dist/boot/stage-05-execution-env.js +20 -0
- package/dist/boot/stage-06-runners.d.ts +143 -0
- package/dist/boot/stage-06-runners.js +207 -0
- package/dist/boot/stage-07-capability-layer.d.ts +157 -0
- package/dist/boot/stage-07-capability-layer.js +162 -0
- package/dist/boot/stage-08-reapers.d.ts +172 -0
- package/dist/boot/stage-08-reapers.js +78 -0
- package/dist/boot/stage-09-leader.d.ts +204 -0
- package/dist/boot/stage-09-leader.js +47 -0
- package/dist/boot/stage-10-http-server.d.ts +204 -0
- package/dist/boot/stage-10-http-server.js +289 -0
- package/dist/config-invariants.d.ts +9 -0
- package/dist/config-invariants.js +7 -1
- package/dist/config-types.d.ts +1 -1
- package/dist/http/dispatch.js +28 -8
- package/dist/http/route-ctx.d.ts +101 -4
- package/dist/http/route-ctx.js +4 -0
- package/dist/http/route-table.d.ts +71 -43
- package/dist/http/route-table.js +111 -187
- package/dist/http/routes/a2a-serve.d.ts +18 -3
- package/dist/http/routes/a2a-serve.js +25 -23
- package/dist/http/routes/admin-config-refresh.d.ts +9 -2
- package/dist/http/routes/admin-config-refresh.js +32 -31
- package/dist/http/routes/admin-drain.d.ts +9 -2
- package/dist/http/routes/admin-drain.js +32 -31
- package/dist/http/routes/admin-memory-optout.d.ts +28 -2
- package/dist/http/routes/admin-memory-optout.js +26 -39
- package/dist/http/routes/adoption.d.ts +15 -2
- package/dist/http/routes/adoption.js +52 -52
- package/dist/http/routes/agents-roster.d.ts +9 -2
- package/dist/http/routes/agents-roster.js +82 -80
- package/dist/http/routes/approvals-assistant.d.ts +58 -5
- package/dist/http/routes/approvals-assistant.js +37 -38
- package/dist/http/routes/attachments.d.ts +22 -2
- package/dist/http/routes/attachments.js +81 -72
- package/dist/http/routes/capabilities.d.ts +21 -2
- package/dist/http/routes/capabilities.js +174 -170
- package/dist/http/routes/config-catalog.d.ts +9 -2
- package/dist/http/routes/config-catalog.js +28 -31
- package/dist/http/routes/devices.d.ts +24 -2
- package/dist/http/routes/devices.js +19 -21
- package/dist/http/routes/diagnostics.d.ts +9 -2
- package/dist/http/routes/diagnostics.js +41 -43
- package/dist/http/routes/fleet.d.ts +11 -2
- package/dist/http/routes/fleet.js +23 -22
- package/dist/http/routes/images.d.ts +63 -9
- package/dist/http/routes/images.js +53 -46
- package/dist/http/routes/leader.d.ts +20 -2
- package/dist/http/routes/leader.js +43 -40
- package/dist/http/routes/memory-bundle.d.ts +15 -2
- package/dist/http/routes/memory-bundle.js +38 -38
- package/dist/http/routes/memory-compliance.d.ts +16 -4
- package/dist/http/routes/memory-compliance.js +44 -45
- package/dist/http/routes/memory-consolidation.d.ts +14 -2
- package/dist/http/routes/memory-consolidation.js +66 -66
- package/dist/http/routes/memory-origin.d.ts +23 -4
- package/dist/http/routes/memory-origin.js +65 -65
- package/dist/http/routes/memory-policy.d.ts +21 -2
- package/dist/http/routes/memory-policy.js +119 -115
- package/dist/http/routes/notify-wake.d.ts +21 -11
- package/dist/http/routes/notify-wake.js +118 -116
- package/dist/http/routes/observability.d.ts +16 -2
- package/dist/http/routes/observability.js +54 -51
- package/dist/http/routes/retention-ops.d.ts +19 -2
- package/dist/http/routes/retention-ops.js +20 -22
- package/dist/http/routes/rules.d.ts +36 -2
- package/dist/http/routes/rules.js +22 -24
- package/dist/http/routes/run-memory-capture.d.ts +10 -3
- package/dist/http/routes/run-memory-capture.js +84 -84
- package/dist/http/routes/runs.d.ts +102 -22
- package/dist/http/routes/runs.js +71 -88
- package/dist/http/routes/session-sync.d.ts +41 -2
- package/dist/http/routes/session-sync.js +25 -23
- package/dist/http/routes/sessions-list.d.ts +16 -2
- package/dist/http/routes/sessions-list.js +78 -75
- package/dist/http/routes/sessions.d.ts +77 -2
- package/dist/http/routes/sessions.js +50 -47
- package/dist/http/routes/shared-memory.d.ts +19 -2
- package/dist/http/routes/shared-memory.js +65 -63
- package/dist/http/routes/side-query.d.ts +11 -2
- package/dist/http/routes/side-query.js +81 -79
- package/dist/http/routes/tasks.d.ts +17 -2
- package/dist/http/routes/tasks.js +18 -11
- package/dist/http/routes/trace-usage.d.ts +40 -2
- package/dist/http/routes/trace-usage.js +46 -35
- package/dist/http/routes/workflows.d.ts +35 -3
- package/dist/http/routes/workflows.js +29 -29
- package/dist/main.js +20 -870
- package/dist/service-version.d.ts +5 -0
- package/dist/service-version.js +12 -0
- package/dist/trace/redact.d.ts +7 -6
- package/dist/trace/redact.js +419 -229
- package/package.json +1 -1
package/dist/trace/redact.js
CHANGED
|
@@ -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,
|
|
481
|
-
return Math.max(start + 1, end -
|
|
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.
|
|
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,
|
|
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,
|
|
569
|
+
pos = scanResumeAfterHit(idx, end, back);
|
|
567
570
|
}
|
|
568
571
|
}
|
|
569
572
|
function collectStickySpans(a, text, at, out) {
|
|
570
|
-
const { y,
|
|
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,
|
|
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
|
|
783
|
-
const
|
|
784
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
1038
|
+
while (r < n && b64At(r))
|
|
967
1039
|
r += 1;
|
|
968
1040
|
if (r - q < 4) {
|
|
969
1041
|
r = q0;
|
|
970
|
-
while (r < n &&
|
|
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
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
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
|
|
1135
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1253
|
-
const
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|