fix(policy): reject unsupported policy keys (#87074)

Merged via squash.

Prepared head SHA: 3ab4ff1d8f
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
This commit is contained in:
Gio Della-Libera
2026-06-02 15:01:57 -07:00
committed by GitHub
parent a86a1de849
commit 646974b7d8
3 changed files with 557 additions and 21 deletions

View File

@@ -205,6 +205,8 @@ Each policy field below is optional. A check runs only when the matching rule is
present in `policy.jsonc`. The observed state is existing OpenClaw config or
workspace metadata; policy reports drift but does not rewrite runtime behavior
unless a repair path is explicitly available and enabled.
Policy files are strict: unsupported sections or rule keys are reported as
`policy/policy-jsonc-invalid` instead of being ignored.
Policy overlays keep broad top-level rules global, then let named scope blocks
add stricter normal policy sections for explicit selectors. A scope name is a

View File

@@ -661,7 +661,6 @@ describe("registerPolicyDoctorChecks", () => {
["tools settings array", { tools: { settings: [] } }, "oc://policy.jsonc/tools/settings"],
["tools entries object", { tools: { entries: {} } }, "oc://policy.jsonc/tools/entries"],
["tools profiles array", { tools: { profiles: [] } }, "oc://policy.jsonc/tools/profiles"],
[
"tools profiles allow string",
{ tools: { profiles: { allow: "coding" } } },
@@ -994,6 +993,182 @@ describe("registerPolicyDoctorChecks", () => {
]);
});
it("rejects unsupported policy keys across policy namespaces", async () => {
const cases: readonly {
readonly label: string;
readonly policy: unknown;
readonly target: string;
}[] = [
{ label: "top-level", policy: { channel: {} }, target: "oc://policy.jsonc/channel" },
{
label: "tools top-level",
policy: { tools: { execPolicy: { allowHosts: ["sandbox"] } } },
target: "oc://policy.jsonc/tools/execPolicy",
},
{
label: "tools settings",
policy: { tools: { settings: {} } },
target: "oc://policy.jsonc/tools/settings",
},
{
label: "tools entries",
policy: { tools: { entries: [] } },
target: "oc://policy.jsonc/tools/entries",
},
{
label: "tools profile",
policy: { tools: { profiles: { deny: ["full"] } } },
target: "oc://policy.jsonc/tools/profiles/deny",
},
{
label: "tools exec",
policy: { tools: { exec: { allowShells: ["bash"] } } },
target: "oc://policy.jsonc/tools/exec/allowShells",
},
{
label: "tools fs",
policy: { tools: { fs: { allowOutsideWorkspace: true } } },
target: "oc://policy.jsonc/tools/fs/allowOutsideWorkspace",
},
{
label: "tools alsoAllow",
policy: { tools: { alsoAllow: { denied: ["exec"] } } },
target: "oc://policy.jsonc/tools/alsoAllow/denied",
},
{
label: "channels",
policy: { channels: { allowRules: [] } },
target: "oc://policy.jsonc/channels/allowRules",
},
{
label: "channel deny rule",
policy: { channels: { denyRules: [{ when: { provider: "telegram" }, action: "deny" }] } },
target: "oc://policy.jsonc/channels/denyRules/#0/action",
},
{
label: "channel deny selector",
policy: {
channels: { denyRules: [{ when: { provider: "telegram", channel: "stable" } }] },
},
target: "oc://policy.jsonc/channels/denyRules/#0/when/channel",
},
{
label: "ingress top-level",
policy: { ingress: { directMessages: {} } },
target: "oc://policy.jsonc/ingress/directMessages",
},
{
label: "ingress session",
policy: { ingress: { session: { requiredScope: "per-channel-peer" } } },
target: "oc://policy.jsonc/ingress/session/requiredScope",
},
{
label: "ingress channels",
policy: { ingress: { channels: { allowOpenGroups: false } } },
target: "oc://policy.jsonc/ingress/channels/allowOpenGroups",
},
{ label: "mcp", policy: { mcp: { clients: {} } }, target: "oc://policy.jsonc/mcp/clients" },
{
label: "mcp servers",
policy: { mcp: { servers: { require: ["docs"] } } },
target: "oc://policy.jsonc/mcp/servers/require",
},
{
label: "models",
policy: { models: { modelRefs: {} } },
target: "oc://policy.jsonc/models/modelRefs",
},
{
label: "models providers",
policy: { models: { providers: { require: ["openai"] } } },
target: "oc://policy.jsonc/models/providers/require",
},
{
label: "network",
policy: { network: { publicNetwork: {} } },
target: "oc://policy.jsonc/network/publicNetwork",
},
{
label: "network privateNetwork",
policy: { network: { privateNetwork: { deny: true } } },
target: "oc://policy.jsonc/network/privateNetwork/deny",
},
{
label: "gateway top-level",
policy: { gateway: { bind: { allowNonLoopback: false } } },
target: "oc://policy.jsonc/gateway/bind",
},
{
label: "gateway exposure",
policy: { gateway: { exposure: { allowPublicBind: false } } },
target: "oc://policy.jsonc/gateway/exposure/allowPublicBind",
},
{
label: "gateway auth",
policy: { gateway: { auth: { allowDisabled: false } } },
target: "oc://policy.jsonc/gateway/auth/allowDisabled",
},
{
label: "agents",
policy: { agents: { tools: {} } },
target: "oc://policy.jsonc/agents/tools",
},
{
label: "agents workspace",
policy: { agents: { workspace: { requireReadOnly: true } } },
target: "oc://policy.jsonc/agents/workspace/requireReadOnly",
},
{
label: "dataHandling",
policy: { dataHandling: { logs: { requireRedaction: true } } },
target: "oc://policy.jsonc/dataHandling/logs",
},
{
label: "dataHandling nested",
policy: { dataHandling: { telemetry: { allowCaptureContent: false } } },
target: "oc://policy.jsonc/dataHandling/telemetry/allowCaptureContent",
},
{
label: "secrets",
policy: { secrets: { requireVault: true } },
target: "oc://policy.jsonc/secrets/requireVault",
},
{
label: "auth",
policy: { auth: { providers: {} } },
target: "oc://policy.jsonc/auth/providers",
},
{
label: "auth profiles",
policy: { auth: { profiles: { requireProvider: true } } },
target: "oc://policy.jsonc/auth/profiles/requireProvider",
},
];
for (const testCase of cases) {
const configPath = join(workspaceDir, `${testCase.label.replaceAll(" ", "-")}.jsonc`);
await fs.writeFile(configPath, "{}", "utf-8");
await fs.writeFile(
join(workspaceDir, "policy.jsonc"),
JSON.stringify(testCase.policy),
"utf-8",
);
clearHealthChecksForTest();
resetPolicyDoctorChecksForTest();
const result = await runPolicyChecks(ctx(configPath, cfgWithPolicy()));
expect(result.findings, testCase.label).toEqual([
expect.objectContaining({
checkId: "policy/policy-jsonc-invalid",
severity: "error",
path: "policy.jsonc",
target: testCase.target,
}),
]);
}
});
it("reports a policy hash mismatch when expectedHash is configured", async () => {
const configPath = join(workspaceDir, "openclaw.jsonc");
await fs.writeFile(configPath, "{}", "utf-8");

View File

@@ -523,6 +523,28 @@ const KNOWN_SENSITIVITY_LEVELS = ["public", "internal", "confidential", "restric
const SUPPORTED_TOOL_METADATA = ["risk", "sensitivity", "owner"] as const;
const SUPPORTED_AUTH_PROFILE_METADATA = ["provider", "mode"] as const;
const SUPPORTED_AUTH_PROFILE_MODES = ["api_key", "aws-sdk", "oauth", "token"] as const;
const SUPPORTED_POLICY_SECTIONS = [
"auth",
"agents",
"channels",
"dataHandling",
"gateway",
"ingress",
"mcp",
"models",
"network",
"sandbox",
"scopes",
"secrets",
"tools",
] as const;
const SUPPORTED_GATEWAY_POLICY_SECTIONS = [
"auth",
"controlUi",
"exposure",
"http",
"remote",
] as const;
const SUPPORTED_GATEWAY_HTTP_ENDPOINTS = ["chatCompletions", "responses"] as const;
const SUPPORTED_DM_POLICIES = ["pairing", "allowlist", "open", "disabled"] as const;
const SUPPORTED_DM_SCOPES = [
@@ -1623,6 +1645,17 @@ export function policyContainerShapeFindings(
),
];
}
const unsupportedTopLevel = unsupportedPolicyKey(policy, SUPPORTED_POLICY_SECTIONS);
if (unsupportedTopLevel !== undefined) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/${ocPathSegment(unsupportedTopLevel)}`,
`${policyPath} ${unsupportedTopLevel} is not a supported policy section.`,
`Remove ${unsupportedTopLevel} or use a supported policy section.`,
),
];
}
if (policy.tools !== undefined && !isRecord(policy.tools)) {
return [
policyShapeFinding(
@@ -1634,26 +1667,6 @@ export function policyContainerShapeFindings(
];
}
if (isRecord(policy.tools)) {
if (policy.tools.settings !== undefined && !isRecord(policy.tools.settings)) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/tools/settings`,
`${policyPath} tools.settings must be an object.`,
`Fix ${policyPath} so tools.settings is an object.`,
),
];
}
if (policy.tools.entries !== undefined && !Array.isArray(policy.tools.entries)) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/tools/entries`,
`${policyPath} tools.entries must be an array.`,
`Fix ${policyPath} so tools.entries is an array.`,
),
];
}
const postureFinding = toolPosturePolicyShapeFinding(policy.tools, {
policyDocName,
policyPath,
@@ -1672,6 +1685,19 @@ export function policyContainerShapeFindings(
),
];
}
if (isRecord(policy.channels)) {
const unsupportedChannelKey = unsupportedPolicyKey(policy.channels, ["denyRules"]);
if (unsupportedChannelKey !== undefined) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/channels/${ocPathSegment(unsupportedChannelKey)}`,
`${policyPath} channels.${unsupportedChannelKey} is not supported in channel policy.`,
`Remove channels.${unsupportedChannelKey} or use channels.denyRules.`,
),
];
}
}
if (policy.mcp !== undefined && !isRecord(policy.mcp)) {
return [
policyShapeFinding(
@@ -1682,6 +1708,19 @@ export function policyContainerShapeFindings(
),
];
}
if (isRecord(policy.mcp)) {
const unsupportedMcpKey = unsupportedPolicyKey(policy.mcp, ["servers"]);
if (unsupportedMcpKey !== undefined) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/mcp/${ocPathSegment(unsupportedMcpKey)}`,
`${policyPath} mcp.${unsupportedMcpKey} is not supported in MCP policy.`,
`Remove mcp.${unsupportedMcpKey} or use mcp.servers.`,
),
];
}
}
if (policy.dataHandling !== undefined && !isRecord(policy.dataHandling)) {
return [
policyShapeFinding(
@@ -1714,6 +1753,19 @@ export function policyContainerShapeFindings(
),
];
}
if (isRecord(policy.models)) {
const unsupportedModelsKey = unsupportedPolicyKey(policy.models, ["providers"]);
if (unsupportedModelsKey !== undefined) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/models/${ocPathSegment(unsupportedModelsKey)}`,
`${policyPath} models.${unsupportedModelsKey} is not supported in model policy.`,
`Remove models.${unsupportedModelsKey} or use models.providers.`,
),
];
}
}
if (isRecord(policy.models)) {
const finding = policyStringArrayShapeFinding(policy.models.providers, {
property: "models.providers",
@@ -1737,6 +1789,17 @@ export function policyContainerShapeFindings(
];
}
if (isRecord(policy.network)) {
const unsupportedNetworkKey = unsupportedPolicyKey(policy.network, ["privateNetwork"]);
if (unsupportedNetworkKey !== undefined) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/network/${ocPathSegment(unsupportedNetworkKey)}`,
`${policyPath} network.${unsupportedNetworkKey} is not supported in network policy.`,
`Remove network.${unsupportedNetworkKey} or use network.privateNetwork.`,
),
];
}
if (policy.network.privateNetwork !== undefined && !isRecord(policy.network.privateNetwork)) {
return [
policyShapeFinding(
@@ -1747,6 +1810,21 @@ export function policyContainerShapeFindings(
),
];
}
if (isRecord(policy.network.privateNetwork)) {
const unsupportedPrivateNetworkKey = unsupportedPolicyKey(policy.network.privateNetwork, [
"allow",
]);
if (unsupportedPrivateNetworkKey !== undefined) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/network/privateNetwork/${ocPathSegment(unsupportedPrivateNetworkKey)}`,
`${policyPath} network.privateNetwork.${unsupportedPrivateNetworkKey} is not supported in network policy.`,
`Remove network.privateNetwork.${unsupportedPrivateNetworkKey} or use network.privateNetwork.allow.`,
),
];
}
}
if (
isRecord(policy.network.privateNetwork) &&
policy.network.privateNetwork.allow !== undefined &&
@@ -1772,6 +1850,23 @@ export function policyContainerShapeFindings(
),
];
}
if (isRecord(policy.secrets)) {
const unsupportedSecretsKey = unsupportedPolicyKey(policy.secrets, [
"allowInsecureProviders",
"denySources",
"requireManagedProviders",
]);
if (unsupportedSecretsKey !== undefined) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/secrets/${ocPathSegment(unsupportedSecretsKey)}`,
`${policyPath} secrets.${unsupportedSecretsKey} is not supported in secrets policy.`,
`Remove secrets.${unsupportedSecretsKey} or use a supported secrets policy rule.`,
),
];
}
}
if (policy.auth !== undefined && !isRecord(policy.auth)) {
return [
policyShapeFinding(
@@ -1782,6 +1877,19 @@ export function policyContainerShapeFindings(
),
];
}
if (isRecord(policy.auth)) {
const unsupportedAuthKey = unsupportedPolicyKey(policy.auth, ["profiles"]);
if (unsupportedAuthKey !== undefined) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/auth/${ocPathSegment(unsupportedAuthKey)}`,
`${policyPath} auth.${unsupportedAuthKey} is not supported in auth policy.`,
`Remove auth.${unsupportedAuthKey} or use auth.profiles.`,
),
];
}
}
if (
isRecord(policy.auth) &&
policy.auth.profiles !== undefined &&
@@ -1796,6 +1904,22 @@ export function policyContainerShapeFindings(
),
];
}
if (isRecord(policy.auth) && isRecord(policy.auth.profiles)) {
const unsupportedProfilesKey = unsupportedPolicyKey(policy.auth.profiles, [
"allowModes",
"requireMetadata",
]);
if (unsupportedProfilesKey !== undefined) {
return [
policyShapeFinding(
policyPath,
`oc://${policyDocName}/auth/profiles/${ocPathSegment(unsupportedProfilesKey)}`,
`${policyPath} auth.profiles.${unsupportedProfilesKey} is not supported in auth profile policy.`,
`Remove auth.profiles.${unsupportedProfilesKey} or use a supported auth profile policy rule.`,
),
];
}
}
const sandboxFinding = sandboxPolicyShapeFinding(policy.sandbox, {
policyDocName,
policyPath,
@@ -1867,6 +1991,15 @@ function ingressPolicyShapeFinding(
`Move session ingress rules to top-level ingress; scoped ingress currently supports ingress.channels.*.`,
);
}
const unsupportedIngressKey = unsupportedPolicyKey(value, ["channels", "session"]);
if (unsupportedIngressKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${targetPrefix}/${ocPathSegment(unsupportedIngressKey)}`,
`${params.policyPath} ${propertyPrefix}.${unsupportedIngressKey} is not supported in ingress policy.`,
`Remove ${propertyPrefix}.${unsupportedIngressKey} or use ingress.session or ingress.channels.`,
);
}
for (const section of ["session", "channels"] as const) {
if (value[section] !== undefined && !isRecord(value[section])) {
return policyShapeFinding(
@@ -1878,6 +2011,15 @@ function ingressPolicyShapeFinding(
}
}
const session = isRecord(value.session) ? value.session : {};
const unsupportedSessionKey = unsupportedPolicyKey(session, ["requireDmScope"]);
if (unsupportedSessionKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${targetPrefix}/session/${ocPathSegment(unsupportedSessionKey)}`,
`${params.policyPath} ${propertyPrefix}.session.${unsupportedSessionKey} is not supported in ingress policy.`,
`Remove ${propertyPrefix}.session.${unsupportedSessionKey} or use ${propertyPrefix}.session.requireDmScope.`,
);
}
if (
session.requireDmScope !== undefined &&
!SUPPORTED_DM_SCOPES.includes(session.requireDmScope as (typeof SUPPORTED_DM_SCOPES)[number])
@@ -1890,6 +2032,19 @@ function ingressPolicyShapeFinding(
);
}
const channels = isRecord(value.channels) ? value.channels : {};
const unsupportedChannelsKey = unsupportedPolicyKey(channels, [
"allowDmPolicies",
"denyOpenGroups",
"requireMentionInGroups",
]);
if (unsupportedChannelsKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${targetPrefix}/channels/${ocPathSegment(unsupportedChannelsKey)}`,
`${params.policyPath} ${propertyPrefix}.channels.${unsupportedChannelsKey} is not supported in ingress policy.`,
`Remove ${propertyPrefix}.channels.${unsupportedChannelsKey} or use a supported ingress channel policy rule.`,
);
}
const allowDmPoliciesFinding = policyStringArrayPropertyShapeFinding(channels.allowDmPolicies, {
allowed: SUPPORTED_DM_POLICIES,
policyDocName: params.policyDocName,
@@ -1932,6 +2087,15 @@ function agentsPolicyShapeFinding(
`Fix ${params.policyPath} so agents is an object.`,
);
}
const unsupportedAgentsKey = unsupportedPolicyKey(value, ["workspace"]);
if (unsupportedAgentsKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/agents/${ocPathSegment(unsupportedAgentsKey)}`,
`${params.policyPath} agents.${unsupportedAgentsKey} is not supported in agents policy.`,
`Remove agents.${unsupportedAgentsKey} or use agents.workspace.`,
);
}
const workspaceFinding = agentWorkspacePolicyShapeFinding(value.workspace, {
policyDocName: params.policyDocName,
policyPath: params.policyPath,
@@ -2309,6 +2473,15 @@ function agentWorkspacePolicyShapeFinding(
`Fix ${params.policyPath} so ${params.propertyPrefix} is an object.`,
);
}
const unsupportedWorkspaceKey = unsupportedPolicyKey(value, ["allowedAccess", "denyTools"]);
if (unsupportedWorkspaceKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${params.targetPrefix}/${ocPathSegment(unsupportedWorkspaceKey)}`,
`${params.policyPath} ${params.propertyPrefix}.${unsupportedWorkspaceKey} is not supported in agent workspace policy.`,
`Remove ${params.propertyPrefix}.${unsupportedWorkspaceKey} or use a supported agent workspace policy rule.`,
);
}
const allowedAccess = value.allowedAccess;
if (allowedAccess !== undefined && !Array.isArray(allowedAccess)) {
return policyShapeFinding(
@@ -2371,6 +2544,24 @@ function toolPosturePolicyShapeFinding(
): HealthFinding | undefined {
const targetPrefix = params.targetPrefix ?? "tools";
const propertyPrefix = params.propertyPrefix ?? "tools";
const allowedTopLevel = [
"alsoAllow",
"denyTools",
"elevated",
"exec",
"fs",
"profiles",
"requireMetadata",
];
const unsupportedTopLevel = unsupportedPolicyKey(tools, allowedTopLevel);
if (unsupportedTopLevel !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${targetPrefix}/${ocPathSegment(unsupportedTopLevel)}`,
`${params.policyPath} ${propertyPrefix}.${unsupportedTopLevel} is not supported in tools policy.`,
`Remove ${propertyPrefix}.${unsupportedTopLevel} or use a supported tools policy rule.`,
);
}
for (const section of ["profiles", "fs", "exec", "elevated", "alsoAllow"] as const) {
if (tools[section] !== undefined && !isRecord(tools[section])) {
return policyShapeFinding(
@@ -2383,6 +2574,15 @@ function toolPosturePolicyShapeFinding(
}
const profiles = isRecord(tools.profiles) ? tools.profiles : {};
const unsupportedProfileKey = unsupportedPolicyKey(profiles, ["allow"]);
if (unsupportedProfileKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${targetPrefix}/profiles/${ocPathSegment(unsupportedProfileKey)}`,
`${params.policyPath} ${propertyPrefix}.profiles.${unsupportedProfileKey} is not supported in tools policy.`,
`Remove ${propertyPrefix}.profiles.${unsupportedProfileKey} or use ${propertyPrefix}.profiles.allow.`,
);
}
const profileAllowFinding = policyStringArrayPropertyShapeFinding(profiles.allow, {
allowed: SUPPORTED_TOOL_PROFILES,
policyDocName: params.policyDocName,
@@ -2396,6 +2596,15 @@ function toolPosturePolicyShapeFinding(
}
const fs = isRecord(tools.fs) ? tools.fs : {};
const unsupportedFsKey = unsupportedPolicyKey(fs, ["requireWorkspaceOnly"]);
if (unsupportedFsKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${targetPrefix}/fs/${ocPathSegment(unsupportedFsKey)}`,
`${params.policyPath} ${propertyPrefix}.fs.${unsupportedFsKey} is not supported in tools policy.`,
`Remove ${propertyPrefix}.fs.${unsupportedFsKey} or use ${propertyPrefix}.fs.requireWorkspaceOnly.`,
);
}
if (fs.requireWorkspaceOnly !== undefined && typeof fs.requireWorkspaceOnly !== "boolean") {
return policyShapeFinding(
params.policyPath,
@@ -2406,6 +2615,19 @@ function toolPosturePolicyShapeFinding(
}
const exec = isRecord(tools.exec) ? tools.exec : {};
const unsupportedExecKey = unsupportedPolicyKey(exec, [
"allowHosts",
"allowSecurity",
"requireAsk",
]);
if (unsupportedExecKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${targetPrefix}/exec/${ocPathSegment(unsupportedExecKey)}`,
`${params.policyPath} ${propertyPrefix}.exec.${unsupportedExecKey} is not supported in tools policy.`,
`Remove ${propertyPrefix}.exec.${unsupportedExecKey} or use a supported tools exec policy rule.`,
);
}
const execLists = [
["allowSecurity", SUPPORTED_TOOL_EXEC_SECURITY, "exec security mode"],
["requireAsk", SUPPORTED_TOOL_EXEC_ASK, "exec ask mode"],
@@ -2426,6 +2648,15 @@ function toolPosturePolicyShapeFinding(
}
const elevated = isRecord(tools.elevated) ? tools.elevated : {};
const unsupportedElevatedKey = unsupportedPolicyKey(elevated, ["allow"]);
if (unsupportedElevatedKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${targetPrefix}/elevated/${ocPathSegment(unsupportedElevatedKey)}`,
`${params.policyPath} ${propertyPrefix}.elevated.${unsupportedElevatedKey} is not supported in tools policy.`,
`Remove ${propertyPrefix}.elevated.${unsupportedElevatedKey} or use ${propertyPrefix}.elevated.allow.`,
);
}
if (elevated.allow !== undefined && typeof elevated.allow !== "boolean") {
return policyShapeFinding(
params.policyPath,
@@ -2436,6 +2667,15 @@ function toolPosturePolicyShapeFinding(
}
const alsoAllow = isRecord(tools.alsoAllow) ? tools.alsoAllow : {};
const unsupportedAlsoAllowKey = unsupportedPolicyKey(alsoAllow, ["expected"]);
if (unsupportedAlsoAllowKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${targetPrefix}/alsoAllow/${ocPathSegment(unsupportedAlsoAllowKey)}`,
`${params.policyPath} ${propertyPrefix}.alsoAllow.${unsupportedAlsoAllowKey} is not supported in tools policy.`,
`Remove ${propertyPrefix}.alsoAllow.${unsupportedAlsoAllowKey} or use ${propertyPrefix}.alsoAllow.expected.`,
);
}
const alsoAllowExpectedFinding = policyStringArrayPropertyShapeFinding(alsoAllow.expected, {
policyDocName: params.policyDocName,
policyPath: params.policyPath,
@@ -2608,12 +2848,38 @@ function gatewayPolicyShapeFinding(
);
}
}
const unsupportedGatewayKey = unsupportedPolicyKey(value, SUPPORTED_GATEWAY_POLICY_SECTIONS);
if (unsupportedGatewayKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/gateway/${ocPathSegment(unsupportedGatewayKey)}`,
`${params.policyPath} gateway.${unsupportedGatewayKey} is not supported in Gateway policy.`,
`Remove gateway.${unsupportedGatewayKey} or use a supported Gateway policy section.`,
);
}
const exposure = isRecord(value.exposure) ? value.exposure : {};
const auth = isRecord(value.auth) ? value.auth : {};
const controlUi = isRecord(value.controlUi) ? value.controlUi : {};
const remote = isRecord(value.remote) ? value.remote : {};
const http = isRecord(value.http) ? value.http : {};
for (const [section, sectionValue, allowedKeys] of [
["exposure", exposure, ["allowNonLoopbackBind", "allowTailscaleFunnel"]],
["auth", auth, ["requireAuth", "requireExplicitRateLimit"]],
["controlUi", controlUi, ["allowInsecure"]],
["remote", remote, ["allow"]],
["http", http, ["denyEndpoints", "requireUrlAllowlists"]],
] as const) {
const unsupportedKey = unsupportedPolicyKey(sectionValue, allowedKeys);
if (unsupportedKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/gateway/${section}/${ocPathSegment(unsupportedKey)}`,
`${params.policyPath} gateway.${section}.${unsupportedKey} is not supported in Gateway policy.`,
`Remove gateway.${section}.${unsupportedKey} or use a supported Gateway policy rule.`,
);
}
}
const booleanRules = [
[
"gateway/exposure/allowNonLoopbackBind",
@@ -2700,6 +2966,15 @@ function policyStringArrayShapeFinding(
`Fix ${params.policyPath} so ${params.property} is an object.`,
);
}
const unsupportedKey = unsupportedPolicyKey(value, ["allow", "deny"]);
if (unsupportedKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${params.target}/${ocPathSegment(unsupportedKey)}`,
`${params.policyPath} ${params.property}.${unsupportedKey} is not supported in policy.`,
`Remove ${params.property}.${unsupportedKey} or use ${params.property}.allow or ${params.property}.deny.`,
);
}
for (const key of ["allow", "deny"] as const) {
const entries = value[key];
if (entries === undefined) {
@@ -2857,6 +3132,41 @@ function invalidChannelDenyRuleFindings(
},
];
}
for (const [index, rule] of policy.channels.denyRules.entries()) {
if (!isRecord(rule)) {
continue;
}
const unsupportedRuleKey = unsupportedPolicyKey(rule, ["id", "reason", "when"]);
if (unsupportedRuleKey !== undefined) {
return [
{
checkId: CHECK_IDS.policyInvalidFile,
severity: "error",
message: `${policyPath} channels.denyRules[${index}].${unsupportedRuleKey} is not supported in channel deny rules.`,
source: "policy",
path: policyPath,
target: `oc://${policyDocName}/channels/denyRules/#${index}/${ocPathSegment(unsupportedRuleKey)}`,
fixHint: `Remove channels.denyRules[${index}].${unsupportedRuleKey} or use id, when.provider, and reason.`,
},
];
}
if (isRecord(rule.when)) {
const unsupportedWhenKey = unsupportedPolicyKey(rule.when, ["provider"]);
if (unsupportedWhenKey !== undefined) {
return [
{
checkId: CHECK_IDS.policyInvalidFile,
severity: "error",
message: `${policyPath} channels.denyRules[${index}].when.${unsupportedWhenKey} is not supported in channel deny rules.`,
source: "policy",
path: policyPath,
target: `oc://${policyDocName}/channels/denyRules/#${index}/when/${ocPathSegment(unsupportedWhenKey)}`,
fixHint: `Remove channels.denyRules[${index}].when.${unsupportedWhenKey} or use when.provider.`,
},
];
}
}
}
const invalid = policy.channels.denyRules.findIndex((rule) => !isChannelDenyRule(rule));
if (invalid < 0) {
return [];
@@ -4742,6 +5052,14 @@ function dataHandlingPolicyShapeFindings(
return [];
}
return [
policySectionUnsupportedKeyFinding(policy.dataHandling, {
policyPath,
policyDocName,
propertyPath: "dataHandling",
targetPath: "dataHandling",
sectionName: "data-handling",
allowedKeys: ["memory", "retention", "sensitiveLogging", "telemetry"],
}),
dataHandlingSectionShapeFinding(policy.dataHandling, {
policyPath,
policyDocName,
@@ -4801,6 +5119,29 @@ function dataHandlingPolicyShapeFindings(
].filter((finding): finding is HealthFinding => finding !== undefined);
}
function policySectionUnsupportedKeyFinding(
value: Record<string, unknown>,
params: {
readonly policyPath: string;
readonly policyDocName: string;
readonly propertyPath: string;
readonly targetPath: string;
readonly sectionName: string;
readonly allowedKeys: readonly string[];
},
): HealthFinding | undefined {
const unsupportedKey = unsupportedPolicyKey(value, params.allowedKeys);
if (unsupportedKey === undefined) {
return undefined;
}
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${params.targetPath}/${ocPathSegment(unsupportedKey)}`,
`${params.policyPath} ${params.propertyPath}.${unsupportedKey} is not supported in ${params.sectionName} policy.`,
`Remove ${params.propertyPath}.${unsupportedKey} or use a supported ${params.sectionName} policy rule.`,
);
}
function dataHandlingSectionShapeFinding(
dataHandling: Record<string, unknown>,
params: {
@@ -4834,6 +5175,24 @@ function dataHandlingBooleanShapeFinding(
},
): HealthFinding | undefined {
const value = getPolicyPath(dataHandling, params.path);
if (isRecord(dataHandling) && typeof params.path[0] === "string") {
const section = dataHandling[params.path[0]];
if (isRecord(section) && typeof params.path[1] === "string") {
const sectionPath = params.path.slice(0, -1).join(".");
const unsupportedKey = unsupportedPolicyKey(section, [params.path[1]]);
if (unsupportedKey !== undefined) {
return policyShapeFinding(
params.policyPath,
`oc://${params.policyDocName}/${params.targetPath
.split("/")
.slice(0, -1)
.join("/")}/${ocPathSegment(unsupportedKey)}`,
`${params.policyPath} dataHandling.${sectionPath}.${unsupportedKey} is not supported in data-handling policy.`,
`Remove dataHandling.${sectionPath}.${unsupportedKey} or use ${params.propertyPath}.`,
);
}
}
}
if (value === undefined || typeof value === "boolean") {
return undefined;
}