mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 11:33:58 +00:00
policy: classify doctor fix recommendations
This commit is contained in:
453
extensions/policy/src/doctor/fix-metadata.ts
Normal file
453
extensions/policy/src/doctor/fix-metadata.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
// Policy doctor fix metadata classifies findings before patch builders exist.
|
||||
import { CHECK_IDS, POLICY_CHECK_IDS } from "./metadata.js";
|
||||
|
||||
export type PolicyFixClass =
|
||||
| "automatic"
|
||||
| "reviewRequired"
|
||||
| "manual"
|
||||
| "validateOnly"
|
||||
| "unsupported";
|
||||
|
||||
export type PolicyFixMetadata = {
|
||||
readonly checkId: (typeof POLICY_CHECK_IDS)[number];
|
||||
readonly fixClass: PolicyFixClass;
|
||||
readonly policyPath?: readonly string[];
|
||||
readonly configTargets?: readonly string[];
|
||||
readonly summary: string;
|
||||
};
|
||||
|
||||
const m = (
|
||||
checkId: (typeof POLICY_CHECK_IDS)[number],
|
||||
fixClass: PolicyFixClass,
|
||||
summary: string,
|
||||
options: Omit<PolicyFixMetadata, "checkId" | "fixClass" | "summary"> = {},
|
||||
): PolicyFixMetadata => ({
|
||||
checkId,
|
||||
fixClass,
|
||||
summary,
|
||||
...options,
|
||||
});
|
||||
|
||||
export const POLICY_FIX_METADATA = [
|
||||
m(CHECK_IDS.policyMissingFile, "manual", "Restore or author the approved policy artifact."),
|
||||
m(CHECK_IDS.policyInvalidFile, "manual", "Repair the policy JSONC syntax or schema."),
|
||||
m(
|
||||
CHECK_IDS.policyHashMismatch,
|
||||
"manual",
|
||||
"Restore the approved artifact or update the expected hash after review.",
|
||||
{ configTargets: ["plugins.entries.policy.config.expectedHash"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyAttestationMismatch,
|
||||
"manual",
|
||||
"Review the current attestation and update accepted hashes after approval.",
|
||||
{ configTargets: ["plugins.entries.policy.config.expectedAttestationHash"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyDeniedChannelProvider,
|
||||
"automatic",
|
||||
"Disable product-managed channels matching the denied provider.",
|
||||
{ policyPath: ["channels", "denyRules"], configTargets: ["channels"] },
|
||||
),
|
||||
m(CHECK_IDS.policyDeniedMcpServer, "reviewRequired", "Remove or disable the denied MCP server.", {
|
||||
policyPath: ["mcp", "servers", "deny"],
|
||||
configTargets: ["mcp.servers"],
|
||||
}),
|
||||
m(
|
||||
CHECK_IDS.policyUnapprovedMcpServer,
|
||||
"reviewRequired",
|
||||
"Remove the unapproved MCP server or select an approved replacement.",
|
||||
{ policyPath: ["mcp", "servers", "allow"], configTargets: ["mcp.servers"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyDeniedModelProvider,
|
||||
"reviewRequired",
|
||||
"Remove the model provider or switch references to an approved provider.",
|
||||
{ policyPath: ["models", "providers", "deny"], configTargets: ["models"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyUnapprovedModelProvider,
|
||||
"reviewRequired",
|
||||
"Select an approved model provider.",
|
||||
{ policyPath: ["models", "providers", "allow"], configTargets: ["models"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyPrivateNetworkAccess,
|
||||
"reviewRequired",
|
||||
"Disable the concrete private-network access opt-in.",
|
||||
{ policyPath: ["network", "privateNetwork", "allow"], configTargets: ["network"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyIngressDmPolicyUnapproved,
|
||||
"reviewRequired",
|
||||
"Set channel DM policy to an allowed value.",
|
||||
{ policyPath: ["ingress", "channels", "allowDmPolicies"], configTargets: ["channels"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyIngressDmScopeUnapproved,
|
||||
"reviewRequired",
|
||||
"Move session DM scope to the required or stricter ordered value.",
|
||||
{ policyPath: ["ingress", "session", "requireDmScope"], configTargets: ["ingress"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyIngressOpenGroupsDenied,
|
||||
"automatic",
|
||||
"Disable product-managed open group ingress.",
|
||||
{ policyPath: ["ingress", "channels", "denyOpenGroups"], configTargets: ["channels"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyIngressGroupMentionRequired,
|
||||
"automatic",
|
||||
"Require mention in product-managed group channels.",
|
||||
{ policyPath: ["ingress", "channels", "requireMentionInGroups"], configTargets: ["channels"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyGatewayNonLoopbackBind,
|
||||
"reviewRequired",
|
||||
"Set gateway bind address to loopback when remote exposure is not intended.",
|
||||
{
|
||||
policyPath: ["gateway", "exposure", "allowNonLoopbackBind"],
|
||||
configTargets: ["gateway.bind"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyGatewayAuthDisabled,
|
||||
"manual",
|
||||
"Configure token, password, or trusted-proxy auth.",
|
||||
{ policyPath: ["gateway", "auth", "requireAuth"], configTargets: ["gateway.auth"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyGatewayRateLimitMissing,
|
||||
"reviewRequired",
|
||||
"Add explicit gateway auth rate limits from product defaults.",
|
||||
{
|
||||
policyPath: ["gateway", "auth", "requireExplicitRateLimit"],
|
||||
configTargets: ["gateway.auth.rateLimit"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyGatewayControlUiInsecure,
|
||||
"automatic",
|
||||
"Disable the insecure Control UI toggle.",
|
||||
{ policyPath: ["gateway", "controlUi", "allowInsecure"], configTargets: ["gateway.controlUi"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyGatewayTailscaleFunnel,
|
||||
"reviewRequired",
|
||||
"Disable Tailscale funnel or serve exposure.",
|
||||
{ policyPath: ["gateway", "exposure", "allowTailscaleFunnel"], configTargets: ["tailscale"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyGatewayRemoteEnabled,
|
||||
"automatic",
|
||||
"Disable product-managed remote gateway mode.",
|
||||
{ policyPath: ["gateway", "remote", "allow"], configTargets: ["gateway.remote"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyGatewayHttpEndpointEnabled,
|
||||
"reviewRequired",
|
||||
"Disable denied Gateway HTTP endpoints.",
|
||||
{ policyPath: ["gateway", "http", "denyEndpoints"], configTargets: ["gateway.http"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyGatewayHttpUrlFetchUnrestricted,
|
||||
"manual",
|
||||
"Add URL allowlists for each URL-fetch input.",
|
||||
{ policyPath: ["gateway", "http", "requireUrlAllowlists"], configTargets: ["gateway.http"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyGatewayNodeCommandDenied,
|
||||
"reviewRequired",
|
||||
"Add the command to gateway node denyCommands or update policy after review.",
|
||||
{
|
||||
policyPath: ["gateway", "nodes", "denyCommands"],
|
||||
configTargets: ["gateway.nodes.denyCommands"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyAgentsWorkspaceAccessDenied,
|
||||
"reviewRequired",
|
||||
"Set agent workspace access to an allowed mode.",
|
||||
{ policyPath: ["agents", "workspace", "allowedAccess"], configTargets: ["agents"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyAgentsToolNotDenied,
|
||||
"automatic",
|
||||
"Merge required built-in workspace tool denies.",
|
||||
{ policyPath: ["agents", "workspace", "denyTools"], configTargets: ["agents"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyToolsProfileUnapproved,
|
||||
"reviewRequired",
|
||||
"Set the tool profile to an allowed profile.",
|
||||
{ policyPath: ["tools", "profiles", "allow"], configTargets: ["tools.profile"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyToolsFsWorkspaceOnlyRequired,
|
||||
"reviewRequired",
|
||||
"Set workspace-only filesystem posture when required assets remain readable.",
|
||||
{
|
||||
policyPath: ["tools", "fs", "requireWorkspaceOnly"],
|
||||
configTargets: ["tools.fs.workspaceOnly"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyToolsExecSecurityUnapproved,
|
||||
"reviewRequired",
|
||||
"Set exec security to an allowed value.",
|
||||
{ policyPath: ["tools", "exec", "allowSecurity"], configTargets: ["tools.exec.security"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyToolsExecAskUnapproved,
|
||||
"reviewRequired",
|
||||
"Set exec ask mode to an allowed value.",
|
||||
{ policyPath: ["tools", "exec", "requireAsk"], configTargets: ["tools.exec.ask"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyToolsExecHostUnapproved,
|
||||
"reviewRequired",
|
||||
"Move exec host to an allowed host mode.",
|
||||
{ policyPath: ["tools", "exec", "allowHosts"], configTargets: ["tools.exec.host"] },
|
||||
),
|
||||
m(CHECK_IDS.policyToolsElevatedEnabled, "automatic", "Set tools elevated mode to disabled.", {
|
||||
policyPath: ["tools", "elevated", "allow"],
|
||||
configTargets: ["tools.elevated.enabled"],
|
||||
}),
|
||||
m(
|
||||
CHECK_IDS.policyToolsAlsoAllowMissing,
|
||||
"reviewRequired",
|
||||
"Add expected alsoAllow entries only when policy intentionally grants them.",
|
||||
{ policyPath: ["tools", "alsoAllow", "expected"], configTargets: ["tools.alsoAllow"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyToolsAlsoAllowUnexpected,
|
||||
"reviewRequired",
|
||||
"Remove unexpected alsoAllow entries.",
|
||||
{ policyPath: ["tools", "alsoAllow", "expected"], configTargets: ["tools.alsoAllow"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyToolsRequiredDenyMissing,
|
||||
"automatic",
|
||||
"Merge required built-in deny tool classes.",
|
||||
{ policyPath: ["tools", "denyTools"], configTargets: ["tools.denyTools"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySandboxModeUnapproved,
|
||||
"reviewRequired",
|
||||
"Set sandbox mode to an allowed value.",
|
||||
{ policyPath: ["sandbox", "requireMode"], configTargets: ["sandbox.mode"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySandboxBackendUnapproved,
|
||||
"reviewRequired",
|
||||
"Choose an approved sandbox backend that is installed.",
|
||||
{ policyPath: ["sandbox", "allowBackends"], configTargets: ["sandbox.backend"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySandboxContainerPostureUnobservable,
|
||||
"unsupported",
|
||||
"Add observable container posture evidence before patching.",
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySandboxContainerHostNetworkDenied,
|
||||
"reviewRequired",
|
||||
"Disable container host networking.",
|
||||
{
|
||||
policyPath: ["sandbox", "containers", "denyHostNetwork"],
|
||||
configTargets: ["sandbox.containers"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySandboxContainerNamespaceJoinDenied,
|
||||
"reviewRequired",
|
||||
"Disable joining container namespaces.",
|
||||
{
|
||||
policyPath: ["sandbox", "containers", "denyContainerNamespaceJoin"],
|
||||
configTargets: ["sandbox.containers"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySandboxContainerMountModeRequired,
|
||||
"reviewRequired",
|
||||
"Change required mounts to read-only.",
|
||||
{
|
||||
policyPath: ["sandbox", "containers", "requireReadOnlyMounts"],
|
||||
configTargets: ["sandbox.containers"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySandboxContainerRuntimeSocketMount,
|
||||
"reviewRequired",
|
||||
"Remove container runtime socket binds.",
|
||||
{
|
||||
policyPath: ["sandbox", "containers", "denyContainerRuntimeSocketMounts"],
|
||||
configTargets: ["sandbox.containers"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySandboxContainerUnconfinedProfile,
|
||||
"reviewRequired",
|
||||
"Remove unconfined container profiles.",
|
||||
{
|
||||
policyPath: ["sandbox", "containers", "denyUnconfinedProfiles"],
|
||||
configTargets: ["sandbox.containers"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySandboxBrowserCdpSourceRangeMissing,
|
||||
"manual",
|
||||
"Add an explicit browser CDP source range.",
|
||||
{
|
||||
policyPath: ["sandbox", "browser", "requireCdpSourceRange"],
|
||||
configTargets: ["agents.sandbox.browser"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyDataHandlingRedactionDisabled,
|
||||
"automatic",
|
||||
"Set sensitive logging to a redacting mode.",
|
||||
{
|
||||
policyPath: ["dataHandling", "sensitiveLogging", "requireRedaction"],
|
||||
configTargets: ["logging.redactSensitive"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyDataHandlingTelemetryContentCapture,
|
||||
"automatic",
|
||||
"Disable telemetry content capture.",
|
||||
{
|
||||
policyPath: ["dataHandling", "telemetry", "denyContentCapture"],
|
||||
configTargets: ["diagnostics.otel.captureContent"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyDataHandlingSessionRetentionNotEnforced,
|
||||
"reviewRequired",
|
||||
"Set session maintenance to enforced mode.",
|
||||
{
|
||||
policyPath: ["dataHandling", "retention", "requireSessionMaintenance"],
|
||||
configTargets: ["session.maintenance.mode"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyDataHandlingSessionTranscriptMemory,
|
||||
"reviewRequired",
|
||||
"Disable transcript indexing for the affected agent scope.",
|
||||
{
|
||||
policyPath: ["dataHandling", "memory", "denySessionTranscriptIndexing"],
|
||||
configTargets: ["memory"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySecretsUnmanagedProvider,
|
||||
"manual",
|
||||
"Migrate the secret to a managed provider.",
|
||||
{ policyPath: ["secrets", "requireManagedProviders"], configTargets: ["secrets"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySecretsDeniedProviderSource,
|
||||
"reviewRequired",
|
||||
"Move the secret out of the denied source.",
|
||||
{ policyPath: ["secrets", "denySources"], configTargets: ["secrets"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policySecretsInsecureProvider,
|
||||
"reviewRequired",
|
||||
"Remove insecure provider overrides.",
|
||||
{ policyPath: ["secrets", "allowInsecureProviders"], configTargets: ["secrets"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyAuthProfileInvalidMetadata,
|
||||
"manual",
|
||||
"Add required provider and mode metadata to auth profiles.",
|
||||
{ policyPath: ["auth", "profiles", "requireMetadata"], configTargets: ["auth.profiles"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyAuthProfileUnapprovedMode,
|
||||
"manual",
|
||||
"Change auth mode and credentials through the auth owner flow.",
|
||||
{ policyPath: ["auth", "profiles", "allowModes"], configTargets: ["auth.profiles"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyExecApprovalsMissing,
|
||||
"manual",
|
||||
"Restore an attributable exec-approvals evidence file.",
|
||||
{ policyPath: ["execApprovals", "requireFile"], configTargets: ["exec-approvals.json"] },
|
||||
),
|
||||
m(CHECK_IDS.policyExecApprovalsInvalid, "manual", "Repair the exec approvals evidence artifact."),
|
||||
m(
|
||||
CHECK_IDS.policyExecApprovalsDefaultSecurityUnapproved,
|
||||
"manual",
|
||||
"Update reviewed default approval evidence or policy.",
|
||||
{
|
||||
policyPath: ["execApprovals", "defaults", "allowSecurity"],
|
||||
configTargets: ["exec-approvals.json"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyExecApprovalsAgentSecurityUnapproved,
|
||||
"manual",
|
||||
"Update reviewed agent approval evidence or policy.",
|
||||
{
|
||||
policyPath: ["execApprovals", "agents", "allowSecurity"],
|
||||
configTargets: ["exec-approvals.json"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyExecApprovalsAutoAllowSkillsEnabled,
|
||||
"reviewRequired",
|
||||
"Disable auto-allow skills in the approval owner surface.",
|
||||
{
|
||||
policyPath: ["execApprovals", "agents", "allowAutoAllowSkills"],
|
||||
configTargets: ["exec-approvals.json"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyExecApprovalsAllowlistMissing,
|
||||
"manual",
|
||||
"Add expected approval patterns through approval review.",
|
||||
{
|
||||
policyPath: ["execApprovals", "agents", "allowlist", "expected"],
|
||||
configTargets: ["exec-approvals.json"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyExecApprovalsAllowlistUnexpected,
|
||||
"manual",
|
||||
"Remove unexpected approval patterns through approval review.",
|
||||
{
|
||||
policyPath: ["execApprovals", "agents", "allowlist", "expected"],
|
||||
configTargets: ["exec-approvals.json"],
|
||||
},
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyMissingToolRisk,
|
||||
"manual",
|
||||
"Add tool risk metadata in the owning tool declaration.",
|
||||
{ policyPath: ["tools", "requireMetadata"], configTargets: ["tools"] },
|
||||
),
|
||||
m(CHECK_IDS.policyUnknownToolRisk, "manual", "Use a supported tool risk level.", {
|
||||
policyPath: ["tools", "requireMetadata"],
|
||||
configTargets: ["tools"],
|
||||
}),
|
||||
m(
|
||||
CHECK_IDS.policyMissingToolSensitivity,
|
||||
"manual",
|
||||
"Add tool sensitivity metadata in the owning tool declaration.",
|
||||
{ policyPath: ["tools", "requireMetadata"], configTargets: ["tools"] },
|
||||
),
|
||||
m(
|
||||
CHECK_IDS.policyMissingToolOwner,
|
||||
"manual",
|
||||
"Add owner metadata in the owning tool declaration.",
|
||||
{ policyPath: ["tools", "requireMetadata"], configTargets: ["tools"] },
|
||||
),
|
||||
m(CHECK_IDS.policyUnknownToolSensitivity, "manual", "Use a supported tool sensitivity token.", {
|
||||
policyPath: ["tools", "requireMetadata"],
|
||||
configTargets: ["tools"],
|
||||
}),
|
||||
] as const satisfies readonly PolicyFixMetadata[];
|
||||
|
||||
export const POLICY_FIX_METADATA_BY_CHECK_ID = new Map(
|
||||
POLICY_FIX_METADATA.map((rule) => [rule.checkId, rule] as const),
|
||||
);
|
||||
@@ -1,6 +1,7 @@
|
||||
// Policy doctor metadata tests cover rule metadata.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { POLICY_RULE_METADATA, type PolicyRuleMetadata } from "./metadata.js";
|
||||
import { POLICY_FIX_METADATA, POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js";
|
||||
import { POLICY_CHECK_IDS, POLICY_RULE_METADATA, type PolicyRuleMetadata } from "./metadata.js";
|
||||
|
||||
describe("policy doctor metadata", () => {
|
||||
it("describes strictness for agent-scoped policy fields", () => {
|
||||
@@ -158,4 +159,97 @@ describe("policy doctor metadata", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("classifies every policy finding for fix recommendation coverage", () => {
|
||||
expect(POLICY_FIX_METADATA.map((rule) => rule.checkId)).toHaveLength(
|
||||
new Set(POLICY_FIX_METADATA.map((rule) => rule.checkId)).size,
|
||||
);
|
||||
expect([...POLICY_FIX_METADATA_BY_CHECK_ID.keys()].toSorted()).toEqual(
|
||||
[...POLICY_CHECK_IDS].toSorted(),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps policy fix class assignments explicit", () => {
|
||||
const grouped = Object.groupBy(POLICY_FIX_METADATA, (rule) => rule.fixClass);
|
||||
|
||||
expect({
|
||||
automatic: grouped.automatic?.map((rule) => rule.checkId).toSorted(),
|
||||
manual: grouped.manual?.map((rule) => rule.checkId).toSorted(),
|
||||
reviewRequired: grouped.reviewRequired?.map((rule) => rule.checkId).toSorted(),
|
||||
unsupported: grouped.unsupported?.map((rule) => rule.checkId).toSorted(),
|
||||
validateOnly: grouped.validateOnly?.map((rule) => rule.checkId).toSorted() ?? [],
|
||||
}).toEqual({
|
||||
automatic: [
|
||||
"policy/agents-tool-not-denied",
|
||||
"policy/channels-denied-provider",
|
||||
"policy/data-handling-redaction-disabled",
|
||||
"policy/data-handling-telemetry-content-capture",
|
||||
"policy/gateway-control-ui-insecure",
|
||||
"policy/gateway-remote-enabled",
|
||||
"policy/ingress-group-mention-required",
|
||||
"policy/ingress-open-groups-denied",
|
||||
"policy/tools-elevated-enabled",
|
||||
"policy/tools-required-deny-missing",
|
||||
],
|
||||
manual: [
|
||||
"policy/attestation-hash-mismatch",
|
||||
"policy/auth-profile-invalid-metadata",
|
||||
"policy/auth-profile-unapproved-mode",
|
||||
"policy/exec-approvals-agent-security-unapproved",
|
||||
"policy/exec-approvals-allowlist-missing",
|
||||
"policy/exec-approvals-allowlist-unexpected",
|
||||
"policy/exec-approvals-default-security-unapproved",
|
||||
"policy/exec-approvals-invalid",
|
||||
"policy/exec-approvals-missing",
|
||||
"policy/gateway-auth-disabled",
|
||||
"policy/gateway-http-url-fetch-unrestricted",
|
||||
"policy/policy-hash-mismatch",
|
||||
"policy/policy-jsonc-invalid",
|
||||
"policy/policy-jsonc-missing",
|
||||
"policy/sandbox-browser-cdp-source-range-missing",
|
||||
"policy/secrets-unmanaged-provider",
|
||||
"policy/tools-missing-owner",
|
||||
"policy/tools-missing-risk-level",
|
||||
"policy/tools-missing-sensitivity-token",
|
||||
"policy/tools-unknown-risk-level",
|
||||
"policy/tools-unknown-sensitivity-token",
|
||||
],
|
||||
reviewRequired: [
|
||||
"policy/agents-workspace-access-denied",
|
||||
"policy/data-handling-session-retention-not-enforced",
|
||||
"policy/data-handling-session-transcript-memory-enabled",
|
||||
"policy/exec-approvals-auto-allow-skills-enabled",
|
||||
"policy/gateway-http-endpoint-enabled",
|
||||
"policy/gateway-node-command-denied",
|
||||
"policy/gateway-non-loopback-bind",
|
||||
"policy/gateway-rate-limit-missing",
|
||||
"policy/gateway-tailscale-funnel",
|
||||
"policy/ingress-dm-policy-unapproved",
|
||||
"policy/ingress-dm-scope-unapproved",
|
||||
"policy/mcp-denied-server",
|
||||
"policy/mcp-unapproved-server",
|
||||
"policy/models-denied-provider",
|
||||
"policy/models-unapproved-provider",
|
||||
"policy/network-private-access-enabled",
|
||||
"policy/sandbox-backend-unapproved",
|
||||
"policy/sandbox-container-host-network-denied",
|
||||
"policy/sandbox-container-mount-mode-required",
|
||||
"policy/sandbox-container-namespace-join-denied",
|
||||
"policy/sandbox-container-runtime-socket-mount",
|
||||
"policy/sandbox-container-unconfined-profile",
|
||||
"policy/sandbox-mode-unapproved",
|
||||
"policy/secrets-denied-provider-source",
|
||||
"policy/secrets-insecure-provider",
|
||||
"policy/tools-also-allow-missing",
|
||||
"policy/tools-also-allow-unexpected",
|
||||
"policy/tools-exec-ask-unapproved",
|
||||
"policy/tools-exec-host-unapproved",
|
||||
"policy/tools-exec-security-unapproved",
|
||||
"policy/tools-fs-workspace-only-required",
|
||||
"policy/tools-profile-unapproved",
|
||||
],
|
||||
unsupported: ["policy/sandbox-container-posture-unobservable"],
|
||||
validateOnly: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
scanPolicyIngress,
|
||||
scanPolicyMcpServers,
|
||||
} from "../policy-state.js";
|
||||
import { registerPolicyDoctorChecks, resetPolicyDoctorChecksForTest } from "./register.js";
|
||||
import {
|
||||
evaluatePolicy,
|
||||
registerPolicyDoctorChecks,
|
||||
resetPolicyDoctorChecksForTest,
|
||||
} from "./register.js";
|
||||
|
||||
let workspaceDir: string;
|
||||
let originalOpenClawHome: string | undefined;
|
||||
@@ -1406,8 +1410,18 @@ describe("registerPolicyDoctorChecks", () => {
|
||||
target: "oc://openclaw.config/channels/telegram",
|
||||
requirement: "oc://policy.jsonc/channels/denyRules/#0",
|
||||
fixHint: "Telegram is not approved for this workspace.",
|
||||
fixRecommendation: {
|
||||
fixClass: "automatic",
|
||||
policyPath: ["channels", "denyRules"],
|
||||
configTargets: ["channels"],
|
||||
summary: "Disable product-managed channels matching the denied provider.",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const evaluation = await evaluatePolicy(ctx(configPath, cfg));
|
||||
expect(evaluation.findings[0]).toHaveProperty("fixRecommendation");
|
||||
expect(evaluation.attestedFindings[0]).not.toHaveProperty("fixRecommendation");
|
||||
});
|
||||
|
||||
it("repairs denied enabled channels by disabling them when workspace repairs are enabled", async () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { POLICY_TOOL_GROUPS } from "../tool-policy-conformance.js";
|
||||
const loadFsPromisesModule = createLazyRuntimeModule(() => import("node:fs/promises"));
|
||||
|
||||
import { createPolicyDoctorChecks } from "./checks.js";
|
||||
import { POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js";
|
||||
import {
|
||||
CHECK_IDS,
|
||||
POLICY_CHECK_IDS,
|
||||
@@ -175,7 +176,7 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
|
||||
policyPath,
|
||||
evidence,
|
||||
expectedAttestationHash: settings.expectedAttestationHash,
|
||||
findings,
|
||||
findings: withPolicyFixRecommendations(findings),
|
||||
attestedFindings: findings,
|
||||
};
|
||||
}
|
||||
@@ -194,7 +195,7 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
|
||||
policyPath,
|
||||
evidence,
|
||||
expectedAttestationHash: settings.expectedAttestationHash,
|
||||
findings,
|
||||
findings: withPolicyFixRecommendations(findings),
|
||||
attestedFindings: findings,
|
||||
};
|
||||
}
|
||||
@@ -206,7 +207,7 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
|
||||
policyPath,
|
||||
evidence,
|
||||
expectedAttestationHash: settings.expectedAttestationHash,
|
||||
findings,
|
||||
findings: withPolicyFixRecommendations(findings),
|
||||
attestedFindings: findings,
|
||||
};
|
||||
}
|
||||
@@ -234,7 +235,7 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
|
||||
policy: { value: policy, hash: policyHash },
|
||||
evidence,
|
||||
expectedAttestationHash: settings.expectedAttestationHash,
|
||||
findings,
|
||||
findings: withPolicyFixRecommendations(findings),
|
||||
attestedFindings: findings,
|
||||
};
|
||||
}
|
||||
@@ -343,11 +344,33 @@ async function evaluatePolicyUncached(ctx: HealthCheckContext): Promise<PolicyEv
|
||||
policy: { value: policy, hash: policyHash },
|
||||
evidence,
|
||||
expectedAttestationHash: settings.expectedAttestationHash,
|
||||
findings,
|
||||
findings: withPolicyFixRecommendations(findings),
|
||||
attestedFindings: policyFindings,
|
||||
};
|
||||
}
|
||||
|
||||
function withPolicyFixRecommendations(
|
||||
findings: readonly HealthFinding[],
|
||||
): readonly HealthFinding[] {
|
||||
return findings.map((finding) => {
|
||||
const metadata = POLICY_FIX_METADATA_BY_CHECK_ID.get(
|
||||
finding.checkId as (typeof POLICY_CHECK_IDS)[number],
|
||||
);
|
||||
if (metadata === undefined) {
|
||||
return finding;
|
||||
}
|
||||
return {
|
||||
...finding,
|
||||
fixRecommendation: {
|
||||
fixClass: metadata.fixClass,
|
||||
...(metadata.policyPath !== undefined ? { policyPath: metadata.policyPath } : {}),
|
||||
...(metadata.configTargets !== undefined ? { configTargets: metadata.configTargets } : {}),
|
||||
summary: metadata.summary,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function policyParseFinding(
|
||||
policyPath: string,
|
||||
policyDocName: string,
|
||||
|
||||
@@ -44,6 +44,12 @@ export interface HealthFinding {
|
||||
readonly target?: string;
|
||||
readonly requirement?: string;
|
||||
readonly fixHint?: string;
|
||||
readonly fixRecommendation?: {
|
||||
readonly fixClass: "automatic" | "reviewRequired" | "manual" | "validateOnly" | "unsupported";
|
||||
readonly policyPath?: readonly string[];
|
||||
readonly configTargets?: readonly string[];
|
||||
readonly summary: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type HealthCheckMode = "doctor" | "lint" | "fix";
|
||||
|
||||
Reference in New Issue
Block a user