mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user