@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.
- package/dist/adapters/local-sources.js +1 -0
- package/dist/adapters/raw-evidence-completeness.js +226 -0
- package/dist/adapters/raw-evidence-git-diff.js +90 -0
- package/dist/adapters/raw-evidence-keys.js +92 -0
- package/dist/adapters/raw-evidence-manifest.js +132 -0
- package/dist/adapters/raw-evidence-pack-store.js +136 -0
- package/dist/adapters/raw-evidence-sanitize.js +190 -0
- package/dist/adapters/raw-evidence.js +656 -1257
- package/dist/commands/backfill.js +7 -0
- package/dist/commands/cli-io.js +92 -0
- package/dist/commands/collection-report.js +135 -0
- package/dist/commands/collection-roots.js +153 -0
- package/dist/commands/install-receipts.js +193 -0
- package/dist/commands/install-update.js +305 -0
- package/dist/commands/local-auth.js +268 -0
- package/dist/commands/local-discovery.js +100 -0
- package/dist/commands/local-help.js +281 -0
- package/dist/commands/local.js +170 -1860
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/sessions.js +162 -0
- package/dist/commands/status.js +191 -0
- package/dist/evidence-upload-client.js +43 -2
- package/dist/raw-evidence-staging.js +15 -2
- package/dist/upload-agent-artifacts.js +153 -0
- package/dist/upload-envelope.js +407 -0
- package/dist/upload-evidence-delivery.js +505 -0
- package/dist/upload-http.js +46 -0
- package/dist/upload-session-reports.js +404 -0
- package/dist/upload.js +132 -1264
- package/package.json +2 -2
|
@@ -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
|
+
}
|