mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 23:07:29 +00:00
105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
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" };
|
|
}
|