Skip to main content
Glama
policy.ts6.92 kB
/** * @file Policy Validator * @description Validates Plan JSON against safety policies */ import type { MikrotikPlan, DeviceFacts, ValidationResult, ValidationError, PolicyViolation, Warning, } from './types.js'; export function validatePlan( plan: MikrotikPlan, facts: DeviceFacts ): ValidationResult { const errors: ValidationError[] = []; const warnings: Warning[] = []; const policyViolations: PolicyViolation[] = []; // Policy: noLockout if (plan.policy.noLockout) { checkNoLockout(plan, facts, policyViolations, warnings); } // Policy: allowVlanFiltering if (!plan.policy.allowVlanFiltering) { checkVlanFiltering(plan, policyViolations); } // Policy: allowMgmtIpChange if (!plan.policy.allowMgmtIpChange) { checkMgmtIpChange(plan, facts, policyViolations); } // Check prechecks for (const step of plan.steps) { for (const precheck of step.precheck) { const precheckResult = validatePrecheck(precheck, facts); if (!precheckResult.valid) { errors.push({ step: step.id, module: step.module, message: `Precheck failed: ${precheck.description}`, details: precheckResult.reason, }); } } } const valid = errors.length === 0 && policyViolations.filter(v => v.severity === 'blocking').length === 0; return { valid, errors, warnings, policyViolations, }; } function checkNoLockout( plan: MikrotikPlan, facts: DeviceFacts, policyViolations: PolicyViolation[], warnings: Warning[] ): void { // Check if disabling winbox/ssh without address restriction const serviceSteps = plan.steps.filter(s => s.module === 'services'); for (const step of serviceSteps) { const services = step.params.services || []; for (const svc of services) { if (['winbox', 'ssh'].includes(svc.name) && svc.disabled && !svc.address) { policyViolations.push({ policy: 'noLockout', message: `Cannot disable ${svc.name} without address restriction. Risk of lockout.`, severity: 'blocking', }); } } } // Check if changing IP on management interface const ipSteps = plan.steps.filter(s => s.module === 'ip'); const mgmtInterfaces = ['br-lan', 'bridge', 'ether2', 'ether8']; // common mgmt interfaces for (const step of ipSteps) { const addresses = step.params.addresses || []; for (const addr of addresses) { if (mgmtInterfaces.includes(addr.interface)) { warnings.push({ level: 'critical', message: `Changing IP on management interface ${addr.interface}. Ensure new address is accessible.`, module: 'ip', step: step.id, }); } } } } function checkVlanFiltering( plan: MikrotikPlan, policyViolations: PolicyViolation[] ): void { const bridgeSteps = plan.steps.filter(s => s.module === 'bridge'); for (const step of bridgeSteps) { if (step.params.vlanFiltering === true) { policyViolations.push({ policy: 'allowVlanFiltering', message: 'VLAN filtering is disabled by policy. Enabling it may cause connectivity loss.', severity: 'blocking', }); } } } function checkMgmtIpChange( plan: MikrotikPlan, facts: DeviceFacts, policyViolations: PolicyViolation[] ): void { const ipSteps = plan.steps.filter(s => s.module === 'ip'); const mgmtInterfaces = ['br-lan', 'bridge', 'ether2', 'ether8']; for (const step of ipSteps) { const addresses = step.params.addresses || []; for (const addr of addresses) { if (mgmtInterfaces.includes(addr.interface)) { // Check if IP already exists const existing = facts.ipAddresses.find( ip => ip.interface === addr.interface ); if (existing && existing.address !== addr.address) { policyViolations.push({ policy: 'allowMgmtIpChange', message: `Changing management IP on ${addr.interface} is disabled by policy.`, severity: 'blocking', }); } } } } } function validatePrecheck( precheck: any, facts: DeviceFacts ): { valid: boolean; reason?: string } { switch (precheck.type) { case 'interface_exists': { const iface = precheck.params.interface; const exists = facts.interfaces.some(i => i.name === iface); return { valid: exists, reason: exists ? undefined : `Interface ${iface} does not exist`, }; } case 'bridge_exists': { const bridge = precheck.params.bridge; const exists = facts.bridges.some(b => b.name === bridge); return { valid: exists, reason: exists ? undefined : `Bridge ${bridge} does not exist`, }; } case 'vlan_available': { const vlanId = precheck.params.vlanId; const exists = facts.vlans.some(v => v.id === vlanId); return { valid: !exists, reason: exists ? `VLAN ${vlanId} already exists` : undefined, }; } case 'ip_not_conflict': { const address = precheck.params.address; const iface = precheck.params.interface; const exists = facts.ipAddresses.some( ip => ip.interface === iface && ip.address === address ); return { valid: !exists, reason: exists ? `IP ${address} already exists on ${iface}` : undefined, }; } case 'service_port_free': { const port = precheck.params.port; const exists = facts.services.some(s => s.port === port && !s.disabled); return { valid: !exists, reason: exists ? `Port ${port} is already in use` : undefined, }; } case 'mgmt_not_blocked': { const mgmtSubnets = precheck.params.mgmtSubnets || []; // Simplified check: assume valid if mgmt subnets are provided return { valid: mgmtSubnets.length > 0, reason: mgmtSubnets.length === 0 ? 'No management subnets specified' : undefined, }; } default: return { valid: true }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server