@blogic-cz/agent-tools 1.5.6 → 1.5.8
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/README.md +9 -1
- package/dist/az-tool/service.d.ts.map +1 -1
- package/dist/azdo-tool/service.d.ts.map +1 -1
- package/dist/credential-guard/index.d.ts +2 -0
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/db-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/gist.d.ts +13 -0
- package/dist/gh-tool/gist.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/pr/review.d.ts.map +1 -1
- package/dist/gh-tool/release.d.ts +46 -1
- package/dist/gh-tool/release.d.ts.map +1 -1
- package/dist/gh-tool/text-input.d.ts +5 -3
- package/dist/gh-tool/text-input.d.ts.map +1 -1
- package/dist/k8s-tool/service.d.ts.map +1 -1
- package/dist/logs-tool/main.d.ts +1 -2
- package/dist/logs-tool/main.d.ts.map +1 -1
- package/dist/observability-tool/shared.d.ts.map +1 -1
- package/dist/session-tool/main.d.ts +1 -2
- package/dist/session-tool/main.d.ts.map +1 -1
- package/dist/shared/audit.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts +6 -0
- package/dist/shared/content-security.d.ts.map +1 -0
- package/dist/shared/credential-patterns.d.ts +11 -0
- package/dist/shared/credential-patterns.d.ts.map +1 -0
- package/dist/shared/effect-version.d.ts.map +1 -1
- package/dist/shared/error-renderer.d.ts.map +1 -1
- package/dist/shared/format.d.ts.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/output-boundary.d.ts +3 -0
- package/dist/shared/output-boundary.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/audit-tool/main.ts +3 -1
- package/src/az-tool/main.ts +3 -1
- package/src/az-tool/service.ts +9 -0
- package/src/azdo-tool/main.ts +3 -1
- package/src/azdo-tool/service.ts +17 -0
- package/src/credential-guard/claude-hook.ts +2 -1
- package/src/credential-guard/index.ts +50 -60
- package/src/db-tool/main.ts +3 -1
- package/src/db-tool/service.ts +25 -2
- package/src/gh-tool/gist.ts +63 -64
- package/src/gh-tool/main.ts +3 -1
- package/src/gh-tool/pr/core.ts +10 -3
- package/src/gh-tool/pr/review.ts +199 -12
- package/src/gh-tool/release.ts +104 -29
- package/src/gh-tool/text-input.ts +19 -38
- package/src/k8s-tool/main.ts +3 -1
- package/src/k8s-tool/service.ts +25 -0
- package/src/logs-tool/main.ts +7 -2
- package/src/observability-tool/main.ts +5 -7
- package/src/observability-tool/shared.ts +41 -1
- package/src/session-tool/main.ts +18 -3
- package/src/shared/audit.ts +11 -10
- package/src/shared/content-security.ts +91 -0
- package/src/shared/credential-patterns.ts +62 -0
- package/src/shared/effect-version.ts +2 -1
- package/src/shared/error-renderer.ts +15 -2
- package/src/shared/format.ts +33 -6
- package/src/shared/index.ts +5 -0
- package/src/shared/output-boundary.ts +29 -0
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
4
|
-
import {
|
|
5
|
-
import type { Cause } from "effect";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
6
5
|
import { Command } from "effect/unstable/cli";
|
|
7
6
|
|
|
8
7
|
import { ConfigServiceLayer } from "#config";
|
|
9
8
|
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
10
|
-
import {
|
|
9
|
+
import { withRedactedOutput } from "#shared/output-boundary";
|
|
10
|
+
import { makeSchemaCommand, renderCauseToStderr, VERSION } from "#shared";
|
|
11
11
|
|
|
12
12
|
import { metricsCommand } from "./metrics";
|
|
13
13
|
import { logsCommand } from "./logs";
|
|
14
14
|
import { traceCommand } from "./trace";
|
|
15
15
|
|
|
16
|
-
const renderCauseToStderr = (cause: Cause.Cause<unknown>) => Console.error(cause.toString());
|
|
17
|
-
|
|
18
16
|
const commandsCommand = makeSchemaCommand(() => mainCommand);
|
|
19
17
|
|
|
20
18
|
const mainCommand = Command.make("observability-tool", {}).pipe(
|
|
@@ -24,11 +22,11 @@ const mainCommand = Command.make("observability-tool", {}).pipe(
|
|
|
24
22
|
Command.withSubcommands([traceCommand, metricsCommand, logsCommand, commandsCommand]),
|
|
25
23
|
);
|
|
26
24
|
|
|
27
|
-
const cli = Command.run(mainCommand, { version: VERSION });
|
|
25
|
+
const cli = Command.run(mainCommand, { version: VERSION, renderErrors: false });
|
|
28
26
|
|
|
29
27
|
const MainLayer = Layer.mergeAll(BunServices.layer, ConfigServiceLayer, AuditServiceLayer);
|
|
30
28
|
|
|
31
|
-
const program = withAudit("observability", cli).pipe(
|
|
29
|
+
const program = withRedactedOutput(withAudit("observability", cli)).pipe(
|
|
32
30
|
Effect.provide(MainLayer),
|
|
33
31
|
Effect.tapCause(renderCauseToStderr),
|
|
34
32
|
);
|
|
@@ -5,6 +5,7 @@ import { ConfigService, getToolConfig } from "#config";
|
|
|
5
5
|
import type { ObservabilityConfig } from "#config";
|
|
6
6
|
|
|
7
7
|
import { ObservabilityToolError } from "./errors";
|
|
8
|
+
import { unsafeOutboundTextReason } from "#shared/content-security";
|
|
8
9
|
import type {
|
|
9
10
|
DsQueryOpts,
|
|
10
11
|
DsQueryResponse,
|
|
@@ -200,6 +201,45 @@ export function observabilityFetch<T>(
|
|
|
200
201
|
path: string,
|
|
201
202
|
init?: RequestInit,
|
|
202
203
|
): Effect.Effect<T, ObservabilityToolError> {
|
|
204
|
+
const rawBody = typeof init?.body === "string" ? init.body : "";
|
|
205
|
+
const url = `${config.url}${path}`;
|
|
206
|
+
let decodedUrl: string;
|
|
207
|
+
try {
|
|
208
|
+
decodedUrl = decodeURIComponent(url);
|
|
209
|
+
} catch {
|
|
210
|
+
return Effect.fail(
|
|
211
|
+
new ObservabilityToolError({
|
|
212
|
+
cause: new Error("Refusing malformed observability URL encoding."),
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const bodyText = [rawBody];
|
|
217
|
+
const appendJsonText = (value: unknown): void => {
|
|
218
|
+
if (typeof value === "string") {
|
|
219
|
+
bodyText.push(value);
|
|
220
|
+
} else if (Array.isArray(value)) {
|
|
221
|
+
value.forEach(appendJsonText);
|
|
222
|
+
} else if (typeof value === "object" && value !== null) {
|
|
223
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
224
|
+
bodyText.push(key);
|
|
225
|
+
appendJsonText(nested);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
try {
|
|
230
|
+
appendJsonText(JSON.parse(rawBody) as unknown);
|
|
231
|
+
} catch {
|
|
232
|
+
// Non-JSON request bodies are still checked as supplied above.
|
|
233
|
+
}
|
|
234
|
+
const unsafeReason = unsafeOutboundTextReason([url, decodedUrl, ...bodyText].join("\n"));
|
|
235
|
+
if (unsafeReason !== null) {
|
|
236
|
+
return Effect.fail(
|
|
237
|
+
new ObservabilityToolError({
|
|
238
|
+
cause: new Error(`Refusing to send observability request containing ${unsafeReason}.`),
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
203
243
|
return Effect.tryPromise({
|
|
204
244
|
try: async () => {
|
|
205
245
|
const headers = buildHeaders(config.token);
|
|
@@ -210,7 +250,7 @@ export function observabilityFetch<T>(
|
|
|
210
250
|
});
|
|
211
251
|
}
|
|
212
252
|
|
|
213
|
-
const response = await fetch(
|
|
253
|
+
const response = await fetch(url, {
|
|
214
254
|
...init,
|
|
215
255
|
headers,
|
|
216
256
|
});
|
package/src/session-tool/main.ts
CHANGED
|
@@ -15,8 +15,16 @@ import type { MessageSummary, SessionResult, SessionSource } from "./types";
|
|
|
15
15
|
|
|
16
16
|
import { ALL_SESSION_SOURCES } from "./types";
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
makeSchemaCommand,
|
|
20
|
+
formatOption,
|
|
21
|
+
formatOutput,
|
|
22
|
+
logText,
|
|
23
|
+
renderCauseToStderr,
|
|
24
|
+
VERSION,
|
|
25
|
+
} from "#shared";
|
|
19
26
|
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
27
|
+
import { withRedactedOutput } from "#shared/output-boundary";
|
|
20
28
|
import { ResolvedPaths, ResolvedPathsLayer } from "./config";
|
|
21
29
|
import { SessionStorageNotFoundError } from "./errors";
|
|
22
30
|
import { formatDate, SessionService, SessionServiceLayer } from "./service";
|
|
@@ -321,18 +329,25 @@ const mainCommand = Command.make("session-tool", {}).pipe(
|
|
|
321
329
|
|
|
322
330
|
const cli = Command.run(mainCommand, {
|
|
323
331
|
version: VERSION,
|
|
332
|
+
renderErrors: false,
|
|
324
333
|
});
|
|
325
334
|
|
|
326
|
-
|
|
335
|
+
const runWithErrorsDisabled = Command.runWith(mainCommand, {
|
|
327
336
|
version: VERSION,
|
|
337
|
+
renderErrors: false,
|
|
328
338
|
});
|
|
339
|
+
export const run = (args: ReadonlyArray<string>) =>
|
|
340
|
+
withRedactedOutput(runWithErrorsDisabled(args)).pipe(Effect.tapCause(renderCauseToStderr));
|
|
329
341
|
|
|
330
342
|
const MainLayer = AppLayer.pipe(
|
|
331
343
|
Layer.provideMerge(BunServices.layer),
|
|
332
344
|
Layer.provideMerge(AuditServiceLayer),
|
|
333
345
|
);
|
|
334
346
|
|
|
335
|
-
const program = withAudit("session", cli).pipe(
|
|
347
|
+
const program = withRedactedOutput(withAudit("session", cli)).pipe(
|
|
348
|
+
Effect.provide(MainLayer),
|
|
349
|
+
Effect.tapCause(renderCauseToStderr),
|
|
350
|
+
);
|
|
336
351
|
|
|
337
352
|
BunRuntime.runMain(program, {
|
|
338
353
|
disableErrorReporting: true,
|
package/src/shared/audit.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
6
6
|
import { Cause, Context, Effect, Layer } from "effect";
|
|
7
7
|
|
|
8
8
|
import { loadConfig } from "#config";
|
|
9
|
+
import { redactSensitiveText } from "./content-security";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_AUDIT_RETENTION_DAYS = 90;
|
|
11
12
|
const IN_MEMORY_DB_PATH = ":memory:";
|
|
@@ -80,12 +81,12 @@ const isInMemoryDatabase = (dbPath: string): boolean =>
|
|
|
80
81
|
const toAuditEntry = (row: AuditRow): AuditLogEntry => ({
|
|
81
82
|
id: row.id,
|
|
82
83
|
ts: row.ts,
|
|
83
|
-
tool: row.tool,
|
|
84
|
-
project: row.project,
|
|
85
|
-
args: row.args,
|
|
84
|
+
tool: redactSensitiveText(row.tool),
|
|
85
|
+
project: redactSensitiveText(row.project),
|
|
86
|
+
args: redactSensitiveText(row.args),
|
|
86
87
|
duration: row.duration,
|
|
87
88
|
success: row.success === 1,
|
|
88
|
-
error: row.error,
|
|
89
|
+
error: row.error === null ? null : redactSensitiveText(row.error),
|
|
89
90
|
exitCode: row.exit_code,
|
|
90
91
|
});
|
|
91
92
|
|
|
@@ -246,12 +247,12 @@ const createAuditService = (dbPath: string, db: Database | null): AuditServiceSh
|
|
|
246
247
|
db.run(
|
|
247
248
|
"INSERT INTO audit_log (tool, project, args, duration, success, error, exit_code) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
248
249
|
[
|
|
249
|
-
safeToolName(entry.tool),
|
|
250
|
-
entry.project,
|
|
251
|
-
entry.args,
|
|
250
|
+
redactSensitiveText(safeToolName(entry.tool)),
|
|
251
|
+
redactSensitiveText(entry.project),
|
|
252
|
+
redactSensitiveText(entry.args),
|
|
252
253
|
entry.duration,
|
|
253
254
|
entry.success ? 1 : 0,
|
|
254
|
-
entry.error
|
|
255
|
+
entry.error === undefined ? null : redactSensitiveText(entry.error),
|
|
255
256
|
entry.exitCode ?? null,
|
|
256
257
|
],
|
|
257
258
|
);
|
|
@@ -317,7 +318,7 @@ export const withAudit = <A, E, R>(
|
|
|
317
318
|
Effect.suspend(() => {
|
|
318
319
|
const startedAt = Date.now();
|
|
319
320
|
const project = process.cwd();
|
|
320
|
-
const args = process.argv.slice(2).join(" ");
|
|
321
|
+
const args = redactSensitiveText(process.argv.slice(2).join(" "));
|
|
321
322
|
const tool = safeToolName(toolName || deriveToolNameFromArgv());
|
|
322
323
|
|
|
323
324
|
return Effect.matchCauseEffect(program, {
|
|
@@ -332,7 +333,7 @@ export const withAudit = <A, E, R>(
|
|
|
332
333
|
args,
|
|
333
334
|
duration: Date.now() - startedAt,
|
|
334
335
|
success: pureHelp,
|
|
335
|
-
error: pureHelp ? undefined : formatCause(cause),
|
|
336
|
+
error: pureHelp ? undefined : redactSensitiveText(formatCause(cause)),
|
|
336
337
|
exitCode: pureHelp ? 0 : extractExitCode(cause),
|
|
337
338
|
}),
|
|
338
339
|
() => Effect.failCause(cause),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { findSecretMatches } from "./credential-patterns";
|
|
2
|
+
|
|
3
|
+
const SENSITIVE_ENV_NAME =
|
|
4
|
+
/(?:KEY|TOKEN|SECRET|PASS(?:WORD)?|PWD|CREDENTIAL|AUTH|COOKIE|SESSION|PSK)/i;
|
|
5
|
+
const isSensitiveEnvironmentName = (name: string) =>
|
|
6
|
+
name !== "PWD" &&
|
|
7
|
+
name !== "OLDPWD" &&
|
|
8
|
+
name !== "CODEX_SESSION_ID" &&
|
|
9
|
+
SENSITIVE_ENV_NAME.test(name);
|
|
10
|
+
|
|
11
|
+
const sensitiveEnvironmentValues = (environment: Record<string, string | undefined>) =>
|
|
12
|
+
Object.entries(environment)
|
|
13
|
+
.flatMap(([name, value]) =>
|
|
14
|
+
isSensitiveEnvironmentName(name) && value && value.length >= 8 ? [value] : [],
|
|
15
|
+
)
|
|
16
|
+
.toSorted((left, right) => right.length - left.length);
|
|
17
|
+
|
|
18
|
+
export function unsafeOutboundTextReason(
|
|
19
|
+
text: string,
|
|
20
|
+
environment: Record<string, string | undefined> = process.env,
|
|
21
|
+
): string | null {
|
|
22
|
+
if (findSecretMatches(text).length > 0) return "a credential pattern";
|
|
23
|
+
if (sensitiveEnvironmentValues(environment).some((value) => text.includes(value))) {
|
|
24
|
+
return "a credential from the process environment";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const assignments = text.match(/^[A-Za-z_][A-Za-z0-9_]*=.*$/gm) ?? [];
|
|
28
|
+
if (
|
|
29
|
+
assignments.length >= 5 &&
|
|
30
|
+
assignments.some((line) => isSensitiveEnvironmentName(line.split("=", 1)[0] ?? ""))
|
|
31
|
+
) {
|
|
32
|
+
return "an environment dump";
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Remove recognized secret spans and known sensitive process-environment values. */
|
|
38
|
+
export function redactSensitiveText(
|
|
39
|
+
text: string,
|
|
40
|
+
environment: Record<string, string | undefined> = process.env,
|
|
41
|
+
): string {
|
|
42
|
+
const spans = findSecretMatches(text).map(({ start, end }) => ({ start, end }));
|
|
43
|
+
for (const value of sensitiveEnvironmentValues(environment)) {
|
|
44
|
+
let start = text.indexOf(value);
|
|
45
|
+
while (start !== -1) {
|
|
46
|
+
spans.push({ start, end: start + value.length });
|
|
47
|
+
start = text.indexOf(value, start + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (spans.length === 0) return text;
|
|
51
|
+
|
|
52
|
+
spans.sort((left, right) => left.start - right.start || right.end - left.end);
|
|
53
|
+
let result = "";
|
|
54
|
+
let offset = 0;
|
|
55
|
+
for (const span of spans) {
|
|
56
|
+
if (span.start < offset) {
|
|
57
|
+
offset = Math.max(offset, span.end);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
result += text.slice(offset, span.start) + "[REDACTED]";
|
|
61
|
+
offset = span.end;
|
|
62
|
+
}
|
|
63
|
+
return result + text.slice(offset);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Sanitize values before serialization so JSON and TOON retain their structure. */
|
|
67
|
+
export function redactSensitiveValue<T>(
|
|
68
|
+
value: T,
|
|
69
|
+
environment: Record<string, string | undefined> = process.env,
|
|
70
|
+
): T {
|
|
71
|
+
if (typeof value === "string") return redactSensitiveText(value, environment) as T;
|
|
72
|
+
if (Array.isArray(value))
|
|
73
|
+
return value.map((item) => redactSensitiveValue(item, environment)) as T;
|
|
74
|
+
if (value === null || typeof value !== "object") return value;
|
|
75
|
+
if (value instanceof Date) return redactSensitiveText(value.toISOString(), environment) as T;
|
|
76
|
+
|
|
77
|
+
return Object.fromEntries(
|
|
78
|
+
Object.entries(value).map(([key, item]) => {
|
|
79
|
+
const serialized = JSON.stringify(item) ?? "undefined";
|
|
80
|
+
const pair = `${JSON.stringify(key)}: ${serialized}`;
|
|
81
|
+
const valueStart = pair.length - serialized.length;
|
|
82
|
+
const contextualSecret =
|
|
83
|
+
typeof item === "string" &&
|
|
84
|
+
findSecretMatches(pair).some((match) => match.start < valueStart && match.end > valueStart);
|
|
85
|
+
return [
|
|
86
|
+
redactSensitiveText(key, environment),
|
|
87
|
+
contextualSecret ? "[REDACTED]" : redactSensitiveValue(item, environment),
|
|
88
|
+
];
|
|
89
|
+
}),
|
|
90
|
+
) as T;
|
|
91
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Patterns used by the credential guard and shared output redaction. */
|
|
2
|
+
export const SECRET_PATTERNS = [
|
|
3
|
+
{ name: "AWS Access Key", pattern: /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/ },
|
|
4
|
+
{ name: "GitHub Token", pattern: /gh[ps]_[A-Za-z0-9]{36}/ },
|
|
5
|
+
{ name: "GitHub PAT", pattern: /github_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}/ },
|
|
6
|
+
{ name: "OpenAI Key", pattern: /sk-[A-Za-z0-9]{48}/ },
|
|
7
|
+
{
|
|
8
|
+
name: "Generic API Key",
|
|
9
|
+
pattern: /(?:api[_-]?key|apikey)["\s:=]+["']?([A-Za-z0-9_-]{20,})["']?/i,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "Generic Secret",
|
|
13
|
+
pattern:
|
|
14
|
+
/(?:secret|token|password|passwd|pwd)[" \t:=]+["']?(?!\$\{|process\.env|z\.|generate|create|read|get|fetch|import|export|const|function|return|Schema)[^\s"']{32,}["']?/i,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
// eslint-disable-next-line eslint/no-useless-concat -- intentionally split to avoid credential guard self-detection
|
|
18
|
+
name: "Priv" + "ate Key",
|
|
19
|
+
pattern: new RegExp("-----BEGIN.*PRIVATE KEY-----"),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "JWT Token",
|
|
23
|
+
pattern: /(?:["'=:\s]|^)eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
|
|
24
|
+
},
|
|
25
|
+
{ name: "Azure SAS Token", pattern: /[?&]sig=[A-Za-z0-9%+/=]{20,}/ },
|
|
26
|
+
{ name: "GCP Service Account Key", pattern: /"type"\s*:\s*"service_account"/ },
|
|
27
|
+
{
|
|
28
|
+
name: "Slack Webhook URL",
|
|
29
|
+
pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "Discord Webhook URL",
|
|
33
|
+
pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "Database URL",
|
|
37
|
+
pattern: /(?:postgres(?:ql)?|mysql|mongodb):\/\/(?!\$\{)[^:]+:(?!\$\{)[^@]+@/,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function findSecretMatches(
|
|
42
|
+
content: string,
|
|
43
|
+
): Array<{ name: string; start: number; end: number }> {
|
|
44
|
+
const matches: Array<{ name: string; start: number; end: number }> = [];
|
|
45
|
+
for (const { name, pattern } of SECRET_PATTERNS) {
|
|
46
|
+
const scanner = new RegExp(
|
|
47
|
+
pattern.source,
|
|
48
|
+
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`,
|
|
49
|
+
);
|
|
50
|
+
for (const match of content.matchAll(scanner)) {
|
|
51
|
+
const start = match.index;
|
|
52
|
+
let end = start + match[0].length;
|
|
53
|
+
if (name === "Private Key") {
|
|
54
|
+
const remainder = content.slice(end);
|
|
55
|
+
const closing = /^\r?\n?[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/.exec(remainder);
|
|
56
|
+
end += closing?.[0].length ?? remainder.length;
|
|
57
|
+
}
|
|
58
|
+
matches.push({ name, start, end });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return matches;
|
|
62
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { redactSensitiveText } from "./content-security";
|
|
2
3
|
|
|
3
4
|
// eslint-disable-next-line import/no-relative-parent-imports -- package.json lives at project root, outside src/
|
|
4
5
|
import pkg from "../../package.json" with { type: "json" };
|
|
@@ -27,6 +28,6 @@ export const checkEffectVersion = async (): Promise<boolean> => {
|
|
|
27
28
|
|
|
28
29
|
const error = effectVersionError(version);
|
|
29
30
|
if (error === undefined) return true;
|
|
30
|
-
console.error(error);
|
|
31
|
+
console.error(redactSensitiveText(error));
|
|
31
32
|
return false;
|
|
32
33
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Effect } from "effect";
|
|
2
2
|
|
|
3
3
|
import { Cause, Console } from "effect";
|
|
4
|
+
import { redactSensitiveText } from "./content-security";
|
|
4
5
|
|
|
5
6
|
const formatError = (error: unknown): string => {
|
|
6
7
|
if (
|
|
@@ -42,7 +43,19 @@ const formatError = (error: unknown): string => {
|
|
|
42
43
|
|
|
43
44
|
const formatCause = (cause: Cause.Cause<unknown>): string => {
|
|
44
45
|
const firstFailure = cause.reasons.find(Cause.isFailReason);
|
|
45
|
-
if (firstFailure !== undefined)
|
|
46
|
+
if (firstFailure !== undefined) {
|
|
47
|
+
if (
|
|
48
|
+
typeof firstFailure.error === "object" &&
|
|
49
|
+
firstFailure.error !== null &&
|
|
50
|
+
Reflect.get(firstFailure.error, "_tag") === "ShowHelp"
|
|
51
|
+
) {
|
|
52
|
+
const errors = Reflect.get(firstFailure.error, "errors");
|
|
53
|
+
return Array.isArray(errors) && errors.length > 0
|
|
54
|
+
? errors.map(formatError).join("; ")
|
|
55
|
+
: "Help requested";
|
|
56
|
+
}
|
|
57
|
+
return formatError(firstFailure.error);
|
|
58
|
+
}
|
|
46
59
|
|
|
47
60
|
const firstDefect = cause.reasons.find(Cause.isDieReason);
|
|
48
61
|
if (firstDefect !== undefined) {
|
|
@@ -56,4 +69,4 @@ const formatCause = (cause: Cause.Cause<unknown>): string => {
|
|
|
56
69
|
};
|
|
57
70
|
|
|
58
71
|
export const renderCauseToStderr = (cause: Cause.Cause<unknown>): Effect.Effect<void> =>
|
|
59
|
-
Console.error(formatCause(cause));
|
|
72
|
+
Console.error(redactSensitiveText(formatCause(cause)));
|
package/src/shared/format.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Flag } from "effect/unstable/cli";
|
|
2
|
-
import { encode as encodeToon } from "@toon-format/toon";
|
|
2
|
+
import { decode as decodeToon, encode as encodeToon } from "@toon-format/toon";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
4
|
|
|
5
|
+
import { redactSensitiveText, redactSensitiveValue } from "./content-security";
|
|
5
6
|
import type { BaseResult, OutputFormat } from "./types";
|
|
6
7
|
|
|
7
8
|
export const formatOption = Flag.Literals("format", ["toon", "json"]).pipe(
|
|
@@ -11,18 +12,44 @@ export const formatOption = Flag.Literals("format", ["toon", "json"]).pipe(
|
|
|
11
12
|
|
|
12
13
|
export function formatOutput<T extends BaseResult>(result: T, format: OutputFormat): string {
|
|
13
14
|
if (format === "toon") {
|
|
14
|
-
return encodeToon(result);
|
|
15
|
+
return encodeToon(redactSensitiveValue(result));
|
|
15
16
|
}
|
|
16
|
-
return JSON.stringify(result, null, 2);
|
|
17
|
+
return JSON.stringify(redactSensitiveValue(result), null, 2);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function formatAny<T>(data: T, format: OutputFormat): string {
|
|
20
21
|
if (format === "toon") {
|
|
21
|
-
return encodeToon(data);
|
|
22
|
+
return encodeToon(redactSensitiveValue(data));
|
|
22
23
|
}
|
|
23
|
-
return JSON.stringify(data, null, 2);
|
|
24
|
+
return JSON.stringify(redactSensitiveValue(data), null, 2);
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
const redactOutputText = (text: string) => {
|
|
28
|
+
let json: unknown;
|
|
29
|
+
let isJson = false;
|
|
30
|
+
try {
|
|
31
|
+
json = JSON.parse(text);
|
|
32
|
+
isJson = true;
|
|
33
|
+
} catch {
|
|
34
|
+
// Try structured TOON below before treating this as plain text.
|
|
35
|
+
}
|
|
36
|
+
if (isJson) {
|
|
37
|
+
return (
|
|
38
|
+
JSON.stringify(redactSensitiveValue(json), null, text.includes("\n") ? 2 : undefined) ??
|
|
39
|
+
"null"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let toon: unknown;
|
|
44
|
+
try {
|
|
45
|
+
toon = decodeToon(text);
|
|
46
|
+
} catch {
|
|
47
|
+
// Raw logs and status strings are not structured output.
|
|
48
|
+
}
|
|
49
|
+
if (toon !== null && typeof toon === "object") return encodeToon(redactSensitiveValue(toon));
|
|
50
|
+
return redactSensitiveText(text);
|
|
51
|
+
};
|
|
52
|
+
|
|
26
53
|
// `Console.log` drops bytes on a non-blocking pipe: a payload over the pipe buffer arrives
|
|
27
54
|
// truncated at a page boundary, reaching the caller as invalid JSON. Awaiting the write callback
|
|
28
55
|
// makes the stream retry the short write. Never replace this with `Console.log`.
|
|
@@ -30,7 +57,7 @@ export function formatAny<T>(data: T, format: OutputFormat): string {
|
|
|
30
57
|
// instead of a silent exit 0 with partial output.
|
|
31
58
|
export const logText = (text: string) =>
|
|
32
59
|
Effect.callback<undefined>((resume) => {
|
|
33
|
-
process.stdout.write(`${text}\n`, (error) =>
|
|
60
|
+
process.stdout.write(`${redactOutputText(text)}\n`, (error) =>
|
|
34
61
|
resume(
|
|
35
62
|
error && (error as NodeJS.ErrnoException).code !== "EPIPE"
|
|
36
63
|
? Effect.die(error)
|
package/src/shared/index.ts
CHANGED
|
@@ -14,6 +14,11 @@ export { expandPath, runCommand } from "./bun";
|
|
|
14
14
|
export { commonArgOptions, parseCommonArgs } from "./cli";
|
|
15
15
|
|
|
16
16
|
export { renderCauseToStderr } from "./error-renderer";
|
|
17
|
+
export {
|
|
18
|
+
redactSensitiveText,
|
|
19
|
+
redactSensitiveValue,
|
|
20
|
+
unsafeOutboundTextReason,
|
|
21
|
+
} from "./content-security";
|
|
17
22
|
|
|
18
23
|
export { dumpCommandSchema, makeSchemaCommand, type CommandSchema } from "./schema-dump";
|
|
19
24
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Console, Effect, Terminal } from "effect";
|
|
2
|
+
|
|
3
|
+
import { redactSensitiveText, redactSensitiveValue } from "./content-security";
|
|
4
|
+
|
|
5
|
+
const redactArgument = (value: unknown) =>
|
|
6
|
+
typeof value === "string" ? redactSensitiveText(value) : redactSensitiveValue(value);
|
|
7
|
+
|
|
8
|
+
export const withRedactedOutput = <A, E, R>(program: Effect.Effect<A, E, R>) =>
|
|
9
|
+
Effect.gen(function* () {
|
|
10
|
+
const currentConsole = yield* Console.Console;
|
|
11
|
+
const currentTerminal = yield* Terminal.Terminal;
|
|
12
|
+
const redactedConsole = new Proxy(currentConsole, {
|
|
13
|
+
get(target, key) {
|
|
14
|
+
const member = Reflect.get(target, key);
|
|
15
|
+
return typeof member === "function"
|
|
16
|
+
? (...args: ReadonlyArray<unknown>) => member.apply(target, args.map(redactArgument))
|
|
17
|
+
: member;
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const redactedTerminal = Object.assign(Object.create(currentTerminal), {
|
|
21
|
+
display: (text: string) => currentTerminal.display(redactSensitiveText(text)),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return yield* Effect.provideService(
|
|
25
|
+
Effect.provideService(program, Console.Console, redactedConsole),
|
|
26
|
+
Terminal.Terminal,
|
|
27
|
+
redactedTerminal,
|
|
28
|
+
);
|
|
29
|
+
});
|