import { isPolicyGatedPermission } from "./permissions.js"; import type { LogicSrcPolicyEvaluationInput, LogicSrcPolicyEvaluationResult, LogicSrcRiskBand } from "./types.js"; const WRITE_ACTIONS = new Set([ "social:post:publish", "social:post:delete", "social:dm:send", "email:send", "email:reply", "email:forward", "email:delete", "email:labels:modify", "email:archive" ]); export function riskBandForScore(score: number): LogicSrcRiskBand { if (!Number.isFinite(score)) { return "critical"; } if (score >= 0.75) { return "critical"; } if (score >= 0.5) { return "high"; } if (score >= 0.25) { return "medium"; } return "low"; } export function scoreAccountActionRisk(input: { action: string; externalRecipientCount?: number; newRecipientOrDomain?: boolean; hasAttachment?: boolean; highReachAccount?: boolean; sensitiveKeywordDetected?: boolean; rawCredentialAccessAttempted?: boolean; }) { let score = 0; if (input.externalRecipientCount && input.externalRecipientCount > 0) score += 0.1; if (input.newRecipientOrDomain) score += 0.15; if (input.hasAttachment) score += 0.15; if (input.action === "social:post:publish") score += 0.2; if (input.action.includes("delete")) score += 0.2; if (input.action.includes("dm:read") || input.action.includes("attachments:read")) score += 0.25; if (input.highReachAccount) score += 0.25; if (input.sensitiveKeywordDetected) score += 0.3; if (input.rawCredentialAccessAttempted) score += 0.4; return Math.min(1, Number(score.toFixed(2))); } function normalizeRiskScore(score: number): number { if (!Number.isFinite(score)) { return 1; } return Math.min(1, Math.max(0, score)); } export function evaluateAccountPolicy(input: LogicSrcPolicyEvaluationInput): LogicSrcPolicyEvaluationResult { const riskScore = normalizeRiskScore(input.riskScore ?? scoreAccountActionRisk({ action: input.action })); const grantActive = input.grant && !input.grant.revokedAt && (!input.grant.expiresAt || Date.parse(input.grant.expiresAt) > Date.now()); const hasPermission = Boolean(grantActive && input.grant?.permissions.includes(input.action)); if (!hasPermission) { return { decision: "deny", riskScore, reason: `missing grant for ${input.action}` }; } if (input.dryRun) { return { decision: "allow", riskScore, reason: "dry run with matching grant" }; } const policy = input.grant?.policy.find((entry) => entry.action === input.action); if (policy?.default === "deny") { return { decision: "deny", riskScore, reason: `policy ${policy.id} denies ${input.action}` }; } if (policy?.default === "approval_required") { return { decision: "approval_required", riskScore, reason: `policy ${policy.id} requires approval` }; } if (policy?.default === "allow_if_trusted_agent") { return input.principal?.trusted ? { decision: "allow", riskScore, reason: `policy ${policy.id} allows trusted principal` } : { decision: "approval_required", riskScore, reason: `policy ${policy.id} requires a trusted principal` }; } if (policy?.default === "allow_if_below_risk_score") { const maxRiskScore = typeof policy.conditions?.maxRiskScore === "number" ? policy.conditions.maxRiskScore : 0.25; return riskScore <= maxRiskScore ? { decision: "allow", riskScore, reason: `risk score is within policy ${policy.id}` } : { decision: "approval_required", riskScore, reason: `risk score exceeds policy ${policy.id}` }; } const band = riskBandForScore(riskScore); if (band === "critical") { return { decision: "deny", riskScore, reason: "critical risk requires admin override" }; } if (isPolicyGatedPermission(input.action) || (WRITE_ACTIONS.has(input.action) && band !== "low")) { return { decision: "approval_required", riskScore, reason: "default gate for risky account action" }; } return { decision: "allow", riskScore, reason: "grant allows account action" }; }