mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-20 07:40:35 +08:00
🐛 fix(jvm): 加固诊断命令策略与输出脱敏
在服务端阻断只读连接中的高风险和多行诊断命令,并对诊断事件与错误消息统一脱敏,避免凭证、Authorization 和 PEM 片段泄漏。
This commit is contained in:
@@ -2,12 +2,14 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
formatJVMDiagnosticChunkText,
|
||||
formatJVMDiagnosticChunksForDisplay,
|
||||
formatJVMDiagnosticCommandTypeLabel,
|
||||
formatJVMDiagnosticPhaseLabel,
|
||||
formatJVMDiagnosticRiskLabel,
|
||||
formatJVMDiagnosticSourceLabel,
|
||||
formatJVMDiagnosticTransportLabel,
|
||||
groupJVMDiagnosticPresets,
|
||||
redactJVMDiagnosticOutput,
|
||||
resolveJVMDiagnosticRiskColor,
|
||||
} from "./jvmDiagnosticPresentation";
|
||||
|
||||
@@ -32,6 +34,204 @@ describe("jvmDiagnosticPresentation", () => {
|
||||
).toBe("执行中:thread -n 5");
|
||||
});
|
||||
|
||||
it("redacts sensitive values in diagnostic output chunks", () => {
|
||||
const text = formatJVMDiagnosticChunkText({
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content:
|
||||
"password=secret-token\napiKey: api-key-secret\naccessToken = bearer-secret\nPRIVATE_KEY=-----BEGIN PRIVATE KEY-----raw-key",
|
||||
});
|
||||
|
||||
expect(text).toContain("password=********");
|
||||
expect(text).toContain("apiKey: ********");
|
||||
expect(text).toContain("accessToken = ********");
|
||||
expect(text).toContain("PRIVATE_KEY=********");
|
||||
expect(text).not.toContain("secret-token");
|
||||
expect(text).not.toContain("api-key-secret");
|
||||
expect(text).not.toContain("bearer-secret");
|
||||
expect(text).not.toContain("raw-key");
|
||||
});
|
||||
|
||||
it("redacts JSON, environment, separator and partial PEM sensitive output", () => {
|
||||
const text = redactJVMDiagnosticOutput([
|
||||
'{"password":"json-secret","api_key":"api-json-secret","accessToken":"access-json-secret"}',
|
||||
"DB_PASSWORD=hunter2",
|
||||
"SPRING_DATASOURCE_PASSWORD=spring-secret",
|
||||
"AWS_SECRET_ACCESS_KEY=aws-secret",
|
||||
"api-key: kebab-secret",
|
||||
"api key = spaced-secret",
|
||||
"private.key: dot-secret",
|
||||
"refresh_token=refresh-secret",
|
||||
"secret=foo;bar",
|
||||
"PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nraw-key-line",
|
||||
].join("\n"));
|
||||
|
||||
expect(text).toContain('"password":"********"');
|
||||
expect(text).toContain('"api_key":"********"');
|
||||
expect(text).toContain('"accessToken":"********"');
|
||||
expect(text).toContain("DB_PASSWORD=********");
|
||||
expect(text).toContain("SPRING_DATASOURCE_PASSWORD=********");
|
||||
expect(text).toContain("AWS_SECRET_ACCESS_KEY=********");
|
||||
expect(text).toContain("api-key: ********");
|
||||
expect(text).toContain("api key = ********");
|
||||
expect(text).toContain("private.key: ********");
|
||||
expect(text).toContain("refresh_token=********");
|
||||
expect(text).toContain("secret=********");
|
||||
expect(text).toContain("PRIVATE_KEY=********");
|
||||
expect(text).not.toContain("json-secret");
|
||||
expect(text).not.toContain("api-json-secret");
|
||||
expect(text).not.toContain("access-json-secret");
|
||||
expect(text).not.toContain("hunter2");
|
||||
expect(text).not.toContain("spring-secret");
|
||||
expect(text).not.toContain("aws-secret");
|
||||
expect(text).not.toContain("kebab-secret");
|
||||
expect(text).not.toContain("spaced-secret");
|
||||
expect(text).not.toContain("dot-secret");
|
||||
expect(text).not.toContain("refresh-secret");
|
||||
expect(text).not.toContain("foo;bar");
|
||||
expect(text).not.toContain("raw-key-line");
|
||||
});
|
||||
|
||||
it("redacts PEM continuation across diagnostic chunks", () => {
|
||||
const texts = formatJVMDiagnosticChunksForDisplay([
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nabc123",
|
||||
},
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "def456\n-----END PRIVATE KEY-----",
|
||||
},
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "thread_name=main",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(texts.join("\n")).not.toContain("abc123");
|
||||
expect(texts.join("\n")).not.toContain("def456");
|
||||
expect(texts.join("\n")).not.toContain("PRIVATE KEY");
|
||||
expect(texts[2]).toContain("thread_name=main");
|
||||
});
|
||||
|
||||
it("redacts PEM begin marker split across diagnostic chunks", () => {
|
||||
const texts = formatJVMDiagnosticChunksForDisplay([
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "PRIVATE_KEY=-----BEGIN PRIV",
|
||||
},
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "ATE KEY-----\nabc123\n-----END PRIVATE KEY-----",
|
||||
},
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "thread_name=main",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(texts.join("\n")).not.toContain("BEGIN PRIV");
|
||||
expect(texts.join("\n")).not.toContain("ATE KEY");
|
||||
expect(texts.join("\n")).not.toContain("abc123");
|
||||
expect(texts[2]).toContain("thread_name=main");
|
||||
});
|
||||
|
||||
it("redacts algorithm-prefixed PEM begin marker split across chunks", () => {
|
||||
const texts = formatJVMDiagnosticChunksForDisplay([
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "-----BEGIN RSA PRIV",
|
||||
},
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "ATE KEY-----\nabc123\n-----END RSA PRIVATE KEY-----",
|
||||
},
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "thread_name=main",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(texts.join("\n")).not.toContain("RSA PRIV");
|
||||
expect(texts.join("\n")).not.toContain("ATE KEY");
|
||||
expect(texts.join("\n")).not.toContain("abc123");
|
||||
expect(texts[2]).toContain("thread_name=main");
|
||||
});
|
||||
|
||||
it("redacts algorithm-prefixed PEM markers split after the algorithm and inside key labels", () => {
|
||||
const cases = [
|
||||
["-----BEGIN RSA", " PRIVATE KEY-----\nabc123\n-----END RSA PRIVATE KEY-----"],
|
||||
["-----BEGIN RSA PRIVATE K", "EY-----\nabc123\n-----END RSA PRIVATE KEY-----"],
|
||||
["-----BEGIN OPENSSH", " PRIVATE KEY-----\nabc123\n-----END OPENSSH PRIVATE KEY-----"],
|
||||
["-----BEGIN EC PRIVATE KE", "Y-----\nabc123\n-----END EC PRIVATE KEY-----"],
|
||||
];
|
||||
|
||||
for (const [firstChunk, secondChunk] of cases) {
|
||||
const texts = formatJVMDiagnosticChunksForDisplay([
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: firstChunk,
|
||||
},
|
||||
{
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: secondChunk,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(texts.join("\n")).not.toContain("PRIVATE K");
|
||||
expect(texts.join("\n")).not.toContain("EY-----");
|
||||
expect(texts.join("\n")).not.toContain("abc123");
|
||||
}
|
||||
});
|
||||
|
||||
it("redacts JSON scalar values and URL query parameters", () => {
|
||||
const text = redactJVMDiagnosticOutput(
|
||||
'{"password":123456,"token":true,"credential":null}\nhttps://svc.local/callback?access_token=url-secret&x=1&api_key=query-secret',
|
||||
);
|
||||
|
||||
expect(text).toContain('"password":********');
|
||||
expect(text).toContain('"token":********');
|
||||
expect(text).toContain('"credential":********');
|
||||
expect(text).toContain("access_token=********");
|
||||
expect(text).toContain("api_key=********");
|
||||
expect(text).not.toContain("123456");
|
||||
expect(text).not.toContain("true");
|
||||
expect(text).not.toContain("url-secret");
|
||||
expect(text).not.toContain("query-secret");
|
||||
});
|
||||
|
||||
it("redacts authorization values across text, JSON and query parameters", () => {
|
||||
const text = redactJVMDiagnosticOutput(
|
||||
'Authorization: Bearer header-secret\n{"authorization":"Bearer json-secret"}\nhttps://svc.local/callback?authorization=Bearer%20query-secret',
|
||||
);
|
||||
|
||||
expect(text).toContain("Authorization: ********");
|
||||
expect(text).toContain('"authorization":"********"');
|
||||
expect(text).toContain("authorization=********");
|
||||
expect(text).not.toContain("header-secret");
|
||||
expect(text).not.toContain("json-secret");
|
||||
expect(text).not.toContain("query-secret");
|
||||
});
|
||||
|
||||
it("keeps non-sensitive diagnostic output unchanged", () => {
|
||||
expect(
|
||||
redactJVMDiagnosticOutput(
|
||||
"thread_name=main\nmethod: com.foo.OrderService.submit\ncost=42ms",
|
||||
),
|
||||
).toBe("thread_name=main\nmethod: com.foo.OrderService.submit\ncost=42ms");
|
||||
});
|
||||
|
||||
it("localizes diagnostic status, transport, risk and source labels", () => {
|
||||
expect(formatJVMDiagnosticPhaseLabel("completed")).toBe("已完成");
|
||||
expect(formatJVMDiagnosticTransportLabel("arthas-tunnel")).toBe("Arthas Tunnel");
|
||||
|
||||
@@ -103,6 +103,160 @@ const SOURCE_LABELS: Record<string, string> = {
|
||||
"ai-plan": "AI 计划",
|
||||
};
|
||||
|
||||
const JVM_DIAGNOSTIC_REDACTION_MASK = "********";
|
||||
const JVM_DIAGNOSTIC_SENSITIVE_KEY_PATTERN =
|
||||
"(?:password|passwd|pwd|secret|token|credential|authorization|api[_.\\- \\t]*key|access[_.\\- \\t]*key|private[_.\\- \\t]*key|secret[_.\\- \\t]*key|auth[_.\\- \\t]*key|access[_.\\- \\t]*token|refresh[_.\\- \\t]*token)";
|
||||
const JVM_DIAGNOSTIC_SENSITIVE_KEY_BODY =
|
||||
`[A-Za-z0-9_.\\- \\t]*${JVM_DIAGNOSTIC_SENSITIVE_KEY_PATTERN}[A-Za-z0-9_.\\- \\t]*`;
|
||||
const JVM_DIAGNOSTIC_PEM_BEGIN_PATTERN =
|
||||
/-----BEGIN [^-]*(?:PRIVATE KEY|SECRET|TOKEN|CREDENTIAL)[^-]*-----/i;
|
||||
const JVM_DIAGNOSTIC_PEM_END_PATTERN =
|
||||
/-----END [^-]*(?:PRIVATE KEY|SECRET|TOKEN|CREDENTIAL)[^-]*-----/i;
|
||||
const JVM_DIAGNOSTIC_PEM_BEGIN_PREFIX_PATTERN = /-----BEGIN[\s\S]*$/i;
|
||||
const JVM_DIAGNOSTIC_PEM_END_CONTINUATION_PATTERN =
|
||||
/^[\s\S]*?-----END [^-]*(?:PRIVATE KEY|SECRET|TOKEN|CREDENTIAL)[^-]*-----/i;
|
||||
const JVM_DIAGNOSTIC_COMPLETE_PEM_PATTERN =
|
||||
/-----BEGIN [^-]*(?:PRIVATE KEY|SECRET|TOKEN|CREDENTIAL)[\s\S]*?-----END [^-]*(?:PRIVATE KEY|SECRET|TOKEN|CREDENTIAL)[^-]*-----/gi;
|
||||
const JVM_DIAGNOSTIC_PARTIAL_PEM_PATTERN =
|
||||
/-----BEGIN [^-]*(?:PRIVATE KEY|SECRET|TOKEN|CREDENTIAL)[\s\S]*$/gi;
|
||||
const JVM_DIAGNOSTIC_SENSITIVE_PEM_LABELS = [
|
||||
"PRIVATE KEY",
|
||||
"RSA PRIVATE KEY",
|
||||
"DSA PRIVATE KEY",
|
||||
"EC PRIVATE KEY",
|
||||
"OPENSSH PRIVATE KEY",
|
||||
"ENCRYPTED PRIVATE KEY",
|
||||
"SECRET",
|
||||
"TOKEN",
|
||||
"CREDENTIAL",
|
||||
];
|
||||
const JVM_DIAGNOSTIC_DOUBLE_QUOTED_VALUE_PATTERN = new RegExp(
|
||||
`(")(${JVM_DIAGNOSTIC_SENSITIVE_KEY_BODY})(")([ \\t]*:[ \\t]*)(")((?:\\\\.|[^"\\\\])*)(")`,
|
||||
"gi",
|
||||
);
|
||||
const JVM_DIAGNOSTIC_SINGLE_QUOTED_VALUE_PATTERN = new RegExp(
|
||||
`(')(${JVM_DIAGNOSTIC_SENSITIVE_KEY_BODY})(')([ \\t]*:[ \\t]*)(')((?:\\\\.|[^'\\\\])*)(')`,
|
||||
"gi",
|
||||
);
|
||||
const JVM_DIAGNOSTIC_UNQUOTED_SCALAR_PATTERN = new RegExp(
|
||||
`(["']?)(${JVM_DIAGNOSTIC_SENSITIVE_KEY_BODY})(\\1)([ \\t]*:[ \\t]*)(true|false|null|-?\\d+(?:\\.\\d+)?)`,
|
||||
"gi",
|
||||
);
|
||||
const JVM_DIAGNOSTIC_UNQUOTED_KEY_VALUE_PATTERN = new RegExp(
|
||||
`(^|[\\r\\n,;{\\[?&]|\\s)(${JVM_DIAGNOSTIC_SENSITIVE_KEY_BODY})([ \\t]*[:=][ \\t]*)([^\\r\\n&]*)`,
|
||||
"gi",
|
||||
);
|
||||
|
||||
const redactJVMDiagnosticKeyValues = (value: string): string =>
|
||||
value
|
||||
.replace(
|
||||
JVM_DIAGNOSTIC_DOUBLE_QUOTED_VALUE_PATTERN,
|
||||
(_match, keyOpen: string, key: string, keyClose: string, separator: string, valueOpen: string, _rawValue: string, valueClose: string) =>
|
||||
`${keyOpen}${key}${keyClose}${separator}${valueOpen}${JVM_DIAGNOSTIC_REDACTION_MASK}${valueClose}`,
|
||||
)
|
||||
.replace(
|
||||
JVM_DIAGNOSTIC_SINGLE_QUOTED_VALUE_PATTERN,
|
||||
(_match, keyOpen: string, key: string, keyClose: string, separator: string, valueOpen: string, _rawValue: string, valueClose: string) =>
|
||||
`${keyOpen}${key}${keyClose}${separator}${valueOpen}${JVM_DIAGNOSTIC_REDACTION_MASK}${valueClose}`,
|
||||
)
|
||||
.replace(
|
||||
JVM_DIAGNOSTIC_UNQUOTED_SCALAR_PATTERN,
|
||||
(_match, keyOpen: string, key: string, keyClose: string, separator: string) =>
|
||||
`${keyOpen}${key}${keyClose}${separator}${JVM_DIAGNOSTIC_REDACTION_MASK}`,
|
||||
)
|
||||
.replace(
|
||||
JVM_DIAGNOSTIC_UNQUOTED_KEY_VALUE_PATTERN,
|
||||
(_match, prefix: string, key: string, separator: string) =>
|
||||
`${prefix}${key}${separator}${JVM_DIAGNOSTIC_REDACTION_MASK}`,
|
||||
);
|
||||
|
||||
export type JVMDiagnosticRedactionState = {
|
||||
insideSensitivePem: boolean;
|
||||
sawSensitivePem: boolean;
|
||||
};
|
||||
|
||||
export const createJVMDiagnosticRedactionState = (): JVMDiagnosticRedactionState => ({
|
||||
insideSensitivePem: false,
|
||||
sawSensitivePem: false,
|
||||
});
|
||||
|
||||
const hasSensitivePemBeginPrefix = (value: string): boolean => {
|
||||
const match = value.match(JVM_DIAGNOSTIC_PEM_BEGIN_PREFIX_PATTERN);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
const prefix = match[0];
|
||||
const label = prefix
|
||||
.replace(/^-----BEGIN\s*/i, "")
|
||||
.replace(/-+$/g, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, " ")
|
||||
.toUpperCase();
|
||||
if (
|
||||
!label ||
|
||||
JVM_DIAGNOSTIC_SENSITIVE_PEM_LABELS.some(
|
||||
(item) => item.startsWith(label) || label.startsWith(item),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return new RegExp(
|
||||
`${JVM_DIAGNOSTIC_SENSITIVE_KEY_BODY}[ \t]*[:=][ \t]*-----BEGIN[\\s\\S]*$`,
|
||||
"i",
|
||||
).test(value);
|
||||
};
|
||||
|
||||
const redactJVMDiagnosticOutputWithState = (
|
||||
value: string,
|
||||
state: JVMDiagnosticRedactionState,
|
||||
): string => {
|
||||
let text = value;
|
||||
if (state.insideSensitivePem) {
|
||||
const pemEnd = text.search(JVM_DIAGNOSTIC_PEM_END_PATTERN);
|
||||
if (pemEnd < 0) {
|
||||
return JVM_DIAGNOSTIC_REDACTION_MASK;
|
||||
}
|
||||
state.insideSensitivePem = false;
|
||||
state.sawSensitivePem = true;
|
||||
text = `${JVM_DIAGNOSTIC_REDACTION_MASK}${text.slice(pemEnd).replace(JVM_DIAGNOSTIC_PEM_END_PATTERN, "")}`;
|
||||
} else if (state.sawSensitivePem && JVM_DIAGNOSTIC_PEM_END_PATTERN.test(text)) {
|
||||
text = text.replace(
|
||||
JVM_DIAGNOSTIC_PEM_END_CONTINUATION_PATTERN,
|
||||
JVM_DIAGNOSTIC_REDACTION_MASK,
|
||||
);
|
||||
}
|
||||
|
||||
text = text
|
||||
.replace(JVM_DIAGNOSTIC_COMPLETE_PEM_PATTERN, () => {
|
||||
state.sawSensitivePem = true;
|
||||
return JVM_DIAGNOSTIC_REDACTION_MASK;
|
||||
})
|
||||
.replace(JVM_DIAGNOSTIC_PARTIAL_PEM_PATTERN, (match) => {
|
||||
state.sawSensitivePem = true;
|
||||
state.insideSensitivePem = !JVM_DIAGNOSTIC_PEM_END_PATTERN.test(match);
|
||||
return JVM_DIAGNOSTIC_REDACTION_MASK;
|
||||
});
|
||||
|
||||
if (!state.insideSensitivePem && hasSensitivePemBeginPrefix(text)) {
|
||||
state.insideSensitivePem = true;
|
||||
state.sawSensitivePem = true;
|
||||
text = text.replace(
|
||||
JVM_DIAGNOSTIC_PEM_BEGIN_PREFIX_PATTERN,
|
||||
JVM_DIAGNOSTIC_REDACTION_MASK,
|
||||
);
|
||||
}
|
||||
|
||||
return redactJVMDiagnosticKeyValues(text);
|
||||
};
|
||||
|
||||
export const redactJVMDiagnosticChunkContent = (
|
||||
value?: string | null,
|
||||
state: JVMDiagnosticRedactionState = createJVMDiagnosticRedactionState(),
|
||||
): string => redactJVMDiagnosticOutputWithState(String(value || ""), state);
|
||||
|
||||
export const redactJVMDiagnosticOutput = (value?: string | null): string =>
|
||||
redactJVMDiagnosticChunkContent(value);
|
||||
|
||||
export const formatJVMDiagnosticPresetCategory = (
|
||||
category: JVMDiagnosticPresetCategory,
|
||||
): string => CATEGORY_LABELS[category];
|
||||
@@ -159,14 +313,14 @@ export const groupJVMDiagnosticPresets = (
|
||||
items: presets.filter((item) => item.category === category),
|
||||
}));
|
||||
|
||||
export const formatJVMDiagnosticChunkText = (
|
||||
const formatJVMDiagnosticChunkTextWithContent = (
|
||||
chunk: JVMDiagnosticEventChunk,
|
||||
content: string,
|
||||
): string => {
|
||||
const rawPhase = String(chunk.phase || chunk.event || "").trim();
|
||||
const phase = chunk.phase
|
||||
? formatJVMDiagnosticPhaseLabel(chunk.phase)
|
||||
: formatJVMDiagnosticEventLabel(chunk.event);
|
||||
const content = String(chunk.content || "").trim();
|
||||
if (!rawPhase && !content) {
|
||||
return "空事件";
|
||||
}
|
||||
@@ -178,3 +332,23 @@ export const formatJVMDiagnosticChunkText = (
|
||||
}
|
||||
return `${phase}:${content}`;
|
||||
};
|
||||
|
||||
export const formatJVMDiagnosticChunkText = (
|
||||
chunk: JVMDiagnosticEventChunk,
|
||||
): string =>
|
||||
formatJVMDiagnosticChunkTextWithContent(
|
||||
chunk,
|
||||
redactJVMDiagnosticOutput(chunk.content).trim(),
|
||||
);
|
||||
|
||||
export const formatJVMDiagnosticChunksForDisplay = (
|
||||
chunks: JVMDiagnosticEventChunk[],
|
||||
): string[] => {
|
||||
const state = createJVMDiagnosticRedactionState();
|
||||
return chunks.map((chunk) =>
|
||||
formatJVMDiagnosticChunkTextWithContent(
|
||||
chunk,
|
||||
redactJVMDiagnosticChunkContent(chunk.content, state).trim(),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user