Files
safetynet/packages/core/security.mjs
T

32 lines
4.3 KiB
JavaScript
Raw Normal View History

2026-10-13 10:33:31 +02:00
import { z } from 'zod';
import { isIP } from 'node:net';
export const AgentScopes=['sessions','navigate','read','click','type','screenshot','ai','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group'];
const host=z.string().max(253).regex(/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i);
export const VisualOriginSchema=z.string().max(2048).refine(value=>{try{const u=new URL(value);return u.protocol==='https:'&&!u.username&&!u.password&&!u.search&&!u.hash&&u.origin===value&&!u.hostname.includes('*')&&!u.hostname.endsWith('.')&&!isIP(u.hostname.replace(/^\[|\]$/g,''))&&/^[a-z0-9.-]+$/.test(u.hostname);}catch{return false;}},'Expected a canonical exact HTTPS domain origin');
export const VisualViewportSchema=z.object({width:z.number().int().min(16).max(8192),height:z.number().int().min(16).max(8192)}).strict();
export const VisualReferenceSchema=z.object({hash:z.string().regex(/^[a-f0-9]{16}$/),algorithm:z.literal('phash-dct32-v1'),scope:z.enum(['viewport','login-form']),viewport:VisualViewportSchema.optional()}).strict();
export const VisualBrandSchema=z.object({id:z.string().min(1).max(80),label:z.string().min(1).max(100),allowedOrigins:z.array(VisualOriginSchema).min(1).max(50),references:z.array(VisualReferenceSchema).min(1).max(20),maxDistance:z.number().int().min(0).max(10).default(6),requiredText:z.array(z.string().min(1).max(100)).max(20).default([])}).strict();
export const SigmaRuleSchema=z.object({id:z.string().min(1).max(80),title:z.string().min(1).max(100),level:z.enum(['low','medium','high','critical']),detection:z.object({selection:z.partialRecord(z.enum(['kind','ruleId','host','decision','policyVersion']),z.union([z.string().max(253),z.number().int(),z.boolean()])).refine(v=>Object.keys(v).length>0,'Nonempty metadata selection required'),condition:z.literal('selection')}).strict()}).strict();
export const SecuritySchema=z.object({
clipboard:z.object({enabled:z.boolean().default(true),allowCopy:z.boolean().default(true),allowCut:z.boolean().default(true),allowPaste:z.boolean().default(true),blockSuspiciousCommands:z.boolean().default(true),failClosed:z.literal(true).default(true),maxTextBytes:z.number().int().min(1024).max(1048576).default(1048576)}).strict().prefault({}),
dns:z.object({enabled:z.boolean().default(true),blockedHosts:z.array(host).max(10000).default([]),blockedIPs:z.array(z.string().refine(v=>isIP(v)>0,'Expected exact IPv4 or IPv6 address')).max(10000).default([]),failClosed:z.literal(true).default(true)}).prefault({}),
yara:z.object({enabled:z.boolean().default(false),rules:z.array(z.object({id:z.string().min(1).max(80),name:z.string().min(1).max(100),source:z.string().min(1).max(65536)}).strict()).max(100).default([]),maxScanBytes:z.number().int().min(1024).max(20*1024*1024).default(20*1024*1024)}).prefault({}),
content:z.object({blockedPhrases:z.array(z.string().min(1).max(4096)).max(500).default([])}).prefault({}),
sigma:z.object({rules:z.array(SigmaRuleSchema).max(100).default([])}).prefault({}),
visualPhishing:z.object({enabled:z.boolean().default(false),failClosed:z.literal(true).default(true),brands:z.array(VisualBrandSchema).max(50).default([])}).strict().prefault({})
}).prefault({});
export const AgentsSchema=z.object({enabled:z.boolean().default(false),scopes:z.array(z.enum(AgentScopes)).max(AgentScopes.length).default(['sessions']),allowedHosts:z.array(host).max(200).default([]),allowSensitiveRead:z.boolean().default(false)}).prefault({});
export function dnsAddressBlocked(address,policy){
const normal=v=>{
let result;try{result=new URL(`http://[${v}]/`).hostname.toLowerCase().replace(/^\[|\]$/g,'');}catch{return v.toLowerCase();}
const mapped=/^::ffff:([a-f0-9]{1,4}):([a-f0-9]{1,4})$/.exec(result);
if(mapped){const upper=parseInt(mapped[1],16),lower=parseInt(mapped[2],16);return `${upper>>>8}.${upper&255}.${lower>>>8}.${lower&255}`;}
return result;
};
return policy.security?.dns?.enabled!==false && (policy.security?.dns?.blockedIPs||[]).some(v=>normal(v)===normal(address));
}
export function evaluateSigma(event,rules){return rules.map(r=>SigmaRuleSchema.parse(r)).filter(r=>Object.entries(r.detection.selection).every(([k,v])=>(k==='kind'?event.kind:event.metadata?.[k])===v)).map(r=>({ruleId:r.id,title:r.title,level:r.level}));}