mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
fix(security): separate warning approval identity
This commit is contained in:
@@ -20,7 +20,7 @@ export type InstallPolicyResult =
|
||||
| { blocked?: undefined; warning?: undefined; findings?: InstallPolicyFinding[] }
|
||||
| {
|
||||
blocked?: undefined;
|
||||
warning: { reason: string; fingerprint: string };
|
||||
warning: { reason: string; fingerprint: string; approvalFingerprint: string };
|
||||
findings?: InstallPolicyFinding[];
|
||||
}
|
||||
| {
|
||||
@@ -105,27 +105,32 @@ function truncateFinding(finding: InstallPolicyFinding): InstallPolicyFinding {
|
||||
};
|
||||
}
|
||||
|
||||
function splitFingerprintText(value: string, sourcePath: string): string[] {
|
||||
function fingerprintWarning(reason: string, findings: InstallPolicyFinding[]): string {
|
||||
return createHash("sha256").update(JSON.stringify({ reason, findings })).digest("hex");
|
||||
}
|
||||
|
||||
function normalizeApprovalFingerprintText(value: string, sourcePath: string): string[] {
|
||||
return sourcePath ? value.split(sourcePath) : [value];
|
||||
}
|
||||
|
||||
function fingerprintWarning(
|
||||
function fingerprintWarningApproval(
|
||||
reason: string,
|
||||
findings: InstallPolicyFinding[],
|
||||
sourcePath: string,
|
||||
): string {
|
||||
// Presentation is truncated and capped; approval must bind the complete
|
||||
// validated warning. Physical staging roots vary across equivalent retries,
|
||||
// so fingerprint their position without binding to the ephemeral path bytes.
|
||||
// validated warning while excluding the transport-only staging path.
|
||||
const fingerprintInput = {
|
||||
reason: splitFingerprintText(reason, sourcePath),
|
||||
reason: normalizeApprovalFingerprintText(reason, sourcePath),
|
||||
findings: findings.map((finding) => ({
|
||||
ruleId: splitFingerprintText(finding.ruleId, sourcePath),
|
||||
ruleId: normalizeApprovalFingerprintText(finding.ruleId, sourcePath),
|
||||
severity: finding.severity,
|
||||
message: splitFingerprintText(finding.message, sourcePath),
|
||||
...(finding.file ? { file: splitFingerprintText(finding.file, sourcePath) } : {}),
|
||||
message: normalizeApprovalFingerprintText(finding.message, sourcePath),
|
||||
...(finding.file ? { file: normalizeApprovalFingerprintText(finding.file, sourcePath) } : {}),
|
||||
...(finding.line !== undefined ? { line: finding.line } : {}),
|
||||
...(finding.evidence ? { evidence: splitFingerprintText(finding.evidence, sourcePath) } : {}),
|
||||
...(finding.evidence
|
||||
? { evidence: normalizeApprovalFingerprintText(finding.evidence, sourcePath) }
|
||||
: {}),
|
||||
})),
|
||||
};
|
||||
return createHash("sha256").update(JSON.stringify(fingerprintInput)).digest("hex");
|
||||
@@ -183,7 +188,12 @@ export function parseInstallPolicyResponse(
|
||||
return {
|
||||
warning: {
|
||||
reason: truncateText(reason.data, MAX_REASON_CHARS),
|
||||
fingerprint: fingerprintWarning(reason.data, fullFindings, params.sourcePath),
|
||||
fingerprint: fingerprintWarning(reason.data, fullFindings),
|
||||
approvalFingerprint: fingerprintWarningApproval(
|
||||
reason.data,
|
||||
fullFindings,
|
||||
params.sourcePath,
|
||||
),
|
||||
},
|
||||
...(normalizedFindings.length > 0 ? { findings: normalizedFindings } : {}),
|
||||
};
|
||||
|
||||
@@ -438,6 +438,7 @@ describe("runInstallPolicy", () => {
|
||||
warning: {
|
||||
reason: "review this source",
|
||||
fingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
approvalFingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
},
|
||||
findings: [
|
||||
{
|
||||
@@ -465,9 +466,10 @@ describe("runInstallPolicy", () => {
|
||||
|
||||
expect(first?.warning?.reason).toBe(second?.warning?.reason);
|
||||
expect(first?.warning?.fingerprint).not.toBe(second?.warning?.fingerprint);
|
||||
expect(first?.warning?.approvalFingerprint).not.toBe(second?.warning?.approvalFingerprint);
|
||||
});
|
||||
|
||||
it("keeps warning fingerprints stable across equivalent staging paths", async () => {
|
||||
it("keeps approval fingerprints stable across equivalent staging paths", async () => {
|
||||
const runWarning = async (stagingPath: string) =>
|
||||
await runInstallPolicy({
|
||||
config: configWithPolicy(scriptPath, {
|
||||
@@ -492,7 +494,8 @@ describe("runInstallPolicy", () => {
|
||||
const first = await runWarning("/tmp/openclaw-stage-a");
|
||||
const second = await runWarning("/tmp/openclaw-stage-b");
|
||||
|
||||
expect(first?.warning?.fingerprint).toBe(second?.warning?.fingerprint);
|
||||
expect(first?.warning?.fingerprint).not.toBe(second?.warning?.fingerprint);
|
||||
expect(first?.warning?.approvalFingerprint).toBe(second?.warning?.approvalFingerprint);
|
||||
expect(first?.warning?.reason).not.toBe(second?.warning?.reason);
|
||||
expect(first?.findings).not.toEqual(second?.findings);
|
||||
});
|
||||
@@ -550,6 +553,7 @@ describe("runInstallPolicy", () => {
|
||||
expect(result?.warning).toEqual({
|
||||
reason: "review valid findings",
|
||||
fingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
approvalFingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||
});
|
||||
expect(result?.findings).toEqual([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user