@bli-cockpit/cli 0.2.28 → 0.2.29

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.
@@ -0,0 +1,190 @@
1
+ import { containsSecretLikeContent, redactSecretLikeContent, } from "@bli-cockpit/telemetry-core";
2
+ import { sha256 } from "./raw-evidence-keys.js";
3
+ export function sanitizeTextEvidenceForUpload(options) {
4
+ const originalBytes = options.originalBytes ?? Buffer.from(options.text, "utf8");
5
+ // Deliberately outside the try: the content guard is the cheap check that
6
+ // decides which masking strategy applies, and it is not the thing whose
7
+ // crash the stub below is for.
8
+ const secretLikeContent = containsSecretLikeContent(options.text);
9
+ try {
10
+ const redactionResult = (options.redact ?? redactSecretLikeContent)(options.text, {
11
+ appliedBy: "local_collector",
12
+ redactedFields: options.redactedFields,
13
+ });
14
+ if (options.secretLikeFileName) {
15
+ return maskFileWithSecretLikeName({
16
+ text: options.text,
17
+ originalBytes,
18
+ redactedFields: options.redactedFields,
19
+ redactionResult,
20
+ });
21
+ }
22
+ if (secretLikeContent) {
23
+ return maskSecretBearingLinesOnly({
24
+ text: options.text,
25
+ originalBytes,
26
+ redactedFields: options.redactedFields,
27
+ redactionResult,
28
+ });
29
+ }
30
+ if (redactionResult.redacted) {
31
+ return keepDeterministicRedaction({
32
+ text: options.text,
33
+ originalBytes,
34
+ redactedFields: options.redactedFields,
35
+ redactionResult,
36
+ });
37
+ }
38
+ return { status: "clean", bytes: originalBytes };
39
+ }
40
+ catch {
41
+ return stubForCrashedRedaction({
42
+ text: options.text,
43
+ originalBytes,
44
+ redactedFields: options.redactedFields,
45
+ });
46
+ }
47
+ }
48
+ /**
49
+ * The name itself (`.env.production`, `id_rsa`) is the signal, so nothing in
50
+ * this file is trusted: whatever the redactor left behind is re-checked, and a
51
+ * still-secret-looking result is replaced wholesale by one marker line.
52
+ */
53
+ function maskFileWithSecretLikeName(input) {
54
+ const sanitizedText = input.redactionResult.redacted
55
+ ? input.redactionResult.text
56
+ : input.text;
57
+ const safeText = containsSecretLikeContent(sanitizedText)
58
+ ? "[REDACTED_LINE:secret_redaction_failed]\n"
59
+ : sanitizedText;
60
+ const sanitizedBytes = Buffer.from(safeText, "utf8");
61
+ return {
62
+ status: "redacted",
63
+ bytes: sanitizedBytes,
64
+ redaction: withRedactionContentMetadata(input.redactionResult.metadata ??
65
+ fallbackRedactionMetadata({
66
+ ruleId: "secret_like_file_name",
67
+ originalText: input.text,
68
+ redactedFields: input.redactedFields,
69
+ fullContentRedacted: false,
70
+ }), { originalBytes: input.originalBytes, sanitizedBytes }),
71
+ completenessLabel: "secret_like_name_masked",
72
+ };
73
+ }
74
+ /** Only the lines that matched are replaced; every other line uploads intact. */
75
+ function maskSecretBearingLinesOnly(input) {
76
+ const redaction = input.redactionResult.metadata ??
77
+ fallbackRedactionMetadata({
78
+ ruleId: "secret_like_content_guard",
79
+ originalText: input.text,
80
+ redactedFields: input.redactedFields,
81
+ });
82
+ let maskedText = maskSecretBearingLines(input.text, redaction);
83
+ if (containsSecretLikeContent(maskedText)) {
84
+ maskedText = "[REDACTED_LINE:secret_redaction_failed]\n";
85
+ }
86
+ const sanitizedBytes = Buffer.from(maskedText, "utf8");
87
+ return {
88
+ status: "redacted",
89
+ bytes: sanitizedBytes,
90
+ redaction: withRedactionContentMetadata(redaction, {
91
+ originalBytes: input.originalBytes,
92
+ sanitizedBytes,
93
+ }),
94
+ completenessLabel: "secret_content_masked",
95
+ };
96
+ }
97
+ /** The redactor found and replaced something the content guard did not flag. */
98
+ function keepDeterministicRedaction(input) {
99
+ const sanitizedBytes = Buffer.from(input.redactionResult.text, "utf8");
100
+ return {
101
+ status: "redacted",
102
+ bytes: sanitizedBytes,
103
+ redaction: withRedactionContentMetadata(input.redactionResult.metadata ??
104
+ fallbackRedactionMetadata({
105
+ ruleId: "secret_redaction_failed",
106
+ originalText: input.text,
107
+ redactedFields: input.redactedFields,
108
+ }), { originalBytes: input.originalBytes, sanitizedBytes }),
109
+ completenessLabel: "secret_content_masked",
110
+ };
111
+ }
112
+ function stubForCrashedRedaction(input) {
113
+ const sanitizedBytes = Buffer.from("[REDACTED_LINE:redaction_crashed_stubbed]\n", "utf8");
114
+ return {
115
+ status: "redacted",
116
+ bytes: sanitizedBytes,
117
+ redaction: withRedactionContentMetadata(fallbackRedactionMetadata({
118
+ ruleId: "redaction_crashed_stubbed",
119
+ originalText: input.text,
120
+ redactedFields: input.redactedFields,
121
+ }), { originalBytes: input.originalBytes, sanitizedBytes }),
122
+ completenessLabel: "redaction_crashed_stubbed",
123
+ };
124
+ }
125
+ function maskSecretBearingLines(text, redaction) {
126
+ if (redaction.redacted_ranges.length === 0) {
127
+ return "[REDACTED_LINE:secret_like_content_guard]\n";
128
+ }
129
+ const segments = text.match(/[^\n]*(?:\n|$)/gu)?.filter(Boolean) ?? [];
130
+ let offset = 0;
131
+ return segments
132
+ .map((segment) => {
133
+ const start = offset;
134
+ const end = offset + segment.length;
135
+ offset = end;
136
+ const matched = redaction.redacted_ranges.find((range) => range.start < end && start < range.end);
137
+ if (!matched)
138
+ return segment;
139
+ return `[REDACTED_LINE:${matched.rule_id}]${lineEndingOf(segment)}`;
140
+ })
141
+ .join("");
142
+ }
143
+ function lineEndingOf(segment) {
144
+ if (segment.endsWith("\r\n"))
145
+ return "\r\n";
146
+ if (segment.endsWith("\n"))
147
+ return "\n";
148
+ return "";
149
+ }
150
+ function fallbackRedactionMetadata(options) {
151
+ const fullContentRedacted = options.fullContentRedacted !== false;
152
+ return {
153
+ schema_version: "raw-evidence-redaction.v1",
154
+ status: "sanitized",
155
+ mode: "deterministic_text_replacement",
156
+ applied_by: ["local_collector"],
157
+ rule_counts: [
158
+ {
159
+ rule_id: options.ruleId,
160
+ match_count: 1,
161
+ redacted_char_count: fullContentRedacted
162
+ ? options.originalText.length
163
+ : 0,
164
+ },
165
+ ],
166
+ secret_like_match_count: 1,
167
+ redacted_fields: options.redactedFields,
168
+ redacted_ranges: fullContentRedacted && options.originalText.length > 0
169
+ ? [
170
+ {
171
+ start: 0,
172
+ end: options.originalText.length,
173
+ rule_id: options.ruleId,
174
+ },
175
+ ]
176
+ : [],
177
+ };
178
+ }
179
+ function withRedactionContentMetadata(metadata, options) {
180
+ if (!metadata) {
181
+ throw new Error("redacted evidence is missing redaction metadata");
182
+ }
183
+ return {
184
+ ...metadata,
185
+ original_content_hash_sha256: sha256(options.originalBytes),
186
+ sanitized_content_hash_sha256: sha256(options.sanitizedBytes),
187
+ original_byte_size: options.originalBytes.byteLength,
188
+ sanitized_byte_size: options.sanitizedBytes.byteLength,
189
+ };
190
+ }