first commit

This commit is contained in:
Karl committed 2026-10-13 10:34:48 +02:00
commit ff482ebe23
251 files changed
+19126

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
import { z } from 'zod';
export const BookmarkSchema=z.object({id:z.string().min(1).max(80).regex(/^[a-zA-Z0-9_-]+$/),title:z.string().trim().min(1).max(120).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)),url:z.string().max(2048).url().refine(v=>{try{const u=new URL(v);return ['https:','http:'].includes(u.protocol)&&!u.username&&!u.password&&!u.search&&!u.hash;}catch{return false;}},'Bookmarks require HTTP(S) without credentials, query or fragment').transform(v=>new URL(v).href).refine(v=>v.length<=2048,'Canonical bookmark URL exceeds limit'),folder:z.string().trim().max(80).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)).default('')}).strict();
export const BookmarkItemsSchema=z.array(BookmarkSchema).max(100).refine(items=>new Set(items.map(i=>i.id)).size===items.length,'Bookmark IDs must be unique').refine(items=>Buffer.byteLength(JSON.stringify(items))<=250000,'Bookmark collection exceeds byte budget');
export const PersonalBookmarksSchema=z.object({revision:z.number().int().nonnegative().safe().default(0),items:BookmarkItemsSchema.default([])}).strict().prefault({});
export const BookmarksSchema=z.object({managed:BookmarkItemsSchema.default([]),allowUserAdd:z.boolean().default(true),allowUserEdit:z.boolean().default(true),allowUserDelete:z.boolean().default(true),adminReadUserBookmarks:z.boolean().default(true),personal:PersonalBookmarksSchema}).strict().prefault({}).superRefine((v,ctx)=>{const ids=new Set(v.managed.map(i=>i.id));if(v.personal.items.some(i=>ids.has(i.id)))ctx.addIssue({code:'custom',message:'Managed and personal bookmark IDs must be distinct'});});
import { createPrivateKey,createPublicKey,sign,verify } from 'node:crypto';
export const BookmarkSnapshotSchema=z.object({deviceId:z.string().uuid(),policyVersion:z.number().int().positive(),revision:z.number().int().nonnegative().safe(),expiresAt:z.string().datetime(),items:BookmarkItemsSchema}).strict();
export function signBookmarkSnapshot(payload,privateKey){payload=BookmarkSnapshotSchema.parse(payload);return {payload,signature:sign(null,Buffer.from('SafetyNet:bookmarks:v1\n'+JSON.stringify(payload)),createPrivateKey(privateKey)).toString('base64')};}
export function verifyBookmarkSnapshot(envelope,publicKey,{deviceId,policyVersion,minimumRevision=0,now=Date.now()}){
if(!envelope||typeof envelope.signature!=='string'||envelope.signature.length>200||Buffer.byteLength(JSON.stringify(envelope))>300000)throw Error('Invalid bookmark snapshot');
const payload=BookmarkSnapshotSchema.parse(envelope.payload);
if(JSON.stringify(payload)!==JSON.stringify(envelope.payload)||!verify(null,Buffer.from('SafetyNet:bookmarks:v1\n'+JSON.stringify(payload)),createPublicKey(publicKey),Buffer.from(envelope.signature,'base64')))throw Error('Invalid bookmark signature');
if(payload.deviceId!==deviceId||payload.policyVersion!==policyVersion||payload.revision<minimumRevision||Date.parse(payload.expiresAt)<=now)throw Error('Bookmark snapshot binding, expiry or rollback rejected');
return payload;
}
+33
View File
@@ -0,0 +1,33 @@
import { inspect } from './policy.mjs';
const blocked=ruleId=>({decision:'block',ruleId});
const allowed=()=>({decision:'allow',ruleId:null});
const normalize=text=>text.normalize('NFKC').toLowerCase().replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/g,'').replace(/[`^]/g,'');
// Deterministic indicators of remote/obfuscated shell execution, not a command parser or malware proof.
export function detectClickFix(text){
if(typeof text!=='string'||Buffer.byteLength(text)>1048576||text.includes('\0'))return blocked('clipboard-uninspectable');
const values=[normalize(text)];try{values.push(normalize(decodeURIComponent(text)));}catch{}
for(const value of values){
const command=value.replace(/["']/g,'');
if(/\b(?:powershell|pwsh)(?:\.exe)?\b[\s\S]{0,512}?\s-(?:e|ec|en|enc|enco|encod|encode|encoded|encodedc|encodedco|encodedcom|encodedcomm|encodedcomma|encodedcomman|encodedcommand)(?:\s+|:|=)[a-z0-9+/]+={0,2}/.test(command))return blocked('clickfix-encoded-powershell');
const remote=/https?:\/\//.test(command),download=/\b(?:invoke-webrequest|invoke-restmethod|iwr|irm)\b|\.download(?:string|file)\s*\(/.test(command),execute=/\b(?:invoke-expression|iex)\b/.test(command);
if(remote&&download&&execute)return blocked('clickfix-powershell-remote-execution');
if(/\bmshta(?:\.exe)?\s+(?:https?:\/\/|(?:vbscript|javascript):)/.test(command))return blocked('clickfix-mshta-execution');
if(/\brundll32(?:\.exe)?\b[\s\S]{0,512}?javascript:[\s\S]{0,512}?(?:runhtmlapplication|getobject)/.test(command))return blocked('clickfix-rundll32-script');
if(/\bregsvr32(?:\.exe)?\b[\s\S]{0,512}?\/i:https?:\/\//.test(command))return blocked('clickfix-regsvr32-remote-script');
if(remote&&/\b(?:curl|wget)(?:\.exe)?\b[^\n;]{0,1024}\|\s*(?:sudo\s+)?(?:sh|bash|zsh|pwsh|powershell)(?:\.exe)?\b/.test(command))return blocked('clickfix-download-pipe-shell');
if(remote&&/\b(?:sh|bash|zsh)\s+-c\s+\$\([^\n]{0,1024}\b(?:curl|wget)\b/.test(command))return blocked('clickfix-shell-download-substitution');
if(remote&&(download||/\b(?:curl|wget|certutil|bitsadmin)(?:\.exe)?\b/.test(command))&&/(?:&&|;|\n)\s*(?:(?:start|start-process|sh|bash|\.\/|\.\\)\s*)?[^\s;\n]{1,200}\.(?:exe|ps1|bat|cmd|sh)\b/.test(command))return blocked('clickfix-download-then-execute');
if(remote&&/\b(?:python|python3)(?:\.exe)?\s+-c\b/.test(command)&&/\b(?:exec|eval)\s*\(/.test(command)&&/\b(?:urllib|requests)\b/.test(command))return blocked('clickfix-python-remote-execution');
}
return allowed();
}
export function inspectClipboard(bytes,policy,operation){
if(!['copy','cut','paste'].includes(operation))return blocked('clipboard-operation-denied');
if(!policy||!Number.isFinite(Date.parse(policy.expiresAt))||Date.parse(policy.expiresAt)<=Date.now())return blocked('clipboard-policy-expired');
const settings=policy.security?.clipboard;if(!settings||settings.failClosed!==true)return blocked('clipboard-policy-unavailable');
if(settings.enabled&&!settings[`allow${operation[0].toUpperCase()}${operation.slice(1)}`])return blocked(`clipboard-${operation}-disabled`);
let data,text;try{if(!Buffer.isBuffer(bytes)&&!(bytes instanceof Uint8Array)&&typeof bytes!=='string')throw Error();data=Buffer.from(bytes);if(data.length>Math.min(settings.maxTextBytes,policy.dlp.maxBytes))return blocked('clipboard-size-limit');text=new TextDecoder('utf-8',{fatal:true}).decode(data);if(text.includes('\0'))throw Error();}catch{return blocked('clipboard-uninspectable');}
const verdict=inspect(data,policy);if(verdict.decision!=='allow')return verdict;
return settings.enabled&&settings.blockSuspiciousCommands?detectClickFix(text):allowed();
}
+13
View File
@@ -0,0 +1,13 @@
import {z} from 'zod';
export function diagnosticsEndpointAllowed(value){
try{const u=new URL(value);if(u.username||u.password||u.search||u.hash||!['https:','http:'].includes(u.protocol))return false;
const host=u.hostname.replace(/^\[|\]$/g,'');return u.protocol==='https:'||host==='::1'||/^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(host)&&host.split('.').every(part=>Number(part)<=255);
}catch{return false;}
}
export const DiagnosticsSchema=z.object({
enabled:z.boolean().default(false),mode:z.enum(['operational','debug']).default('operational'),
endpoint:z.string().max(2048).refine(diagnosticsEndpointAllowed,'Collector requires HTTPS or numeric loopback without credentials, query or fragment').optional(),
sampleRate:z.number().min(0).max(1).default(0.1),debugSampleRate:z.number().min(0).max(1).default(1),
maxQueueSize:z.number().int().min(16).max(4096).default(512),flushIntervalMs:z.number().int().min(250).max(10000).default(2000),
maxRecordAgeMs:z.number().int().min(1000).max(300000).default(30000),exportTimeoutMs:z.number().int().min(250).max(10000).default(2000)
}).strict().superRefine((config,context)=>{if(config.enabled&&!config.endpoint)context.addIssue({code:'custom',message:'Enabled diagnostics require a centrally configured collector',path:['endpoint']});});
+24
View File
@@ -0,0 +1,24 @@
import { z } from 'zod';
import { isIP } from 'node:net';
const endpoint=(protocols)=>z.string().max(2048).refine(value=>{try{const u=new URL(value);return protocols.includes(u.protocol)&&!!u.hostname&&!u.username&&!u.password&&!u.hash&&!u.search&&u.pathname.length>1;}catch{return false;}},'Use an absolute service URL without credentials, query or fragment');
export const EnterpriseSchema=z.object({
dns:z.object({mode:z.enum(['system','secure']).default('system'),templates:z.array(endpoint(['https:'])).max(4).default([])}).strict().prefault({}),
vpn:z.object({required:z.boolean().default(false),interfaceNames:z.array(z.string().trim().min(1).max(100)).max(20).default([]),requiredDnsServers:z.array(z.string().refine(v=>isIP(v)>0,'Expected a DNS server IP address')).max(20).default([])}).strict().prefault({}),
icap:z.object({enabled:z.boolean().default(false),url:z.union([z.literal(''),endpoint(['icaps:','icap:'])]).default(''),method:z.enum(['RESPMOD','REQMOD']).default('RESPMOD'),allow204:z.literal(true).default(true),allowInsecureTransport:z.boolean().default(false),scanUploads:z.boolean().default(true),scanDownloads:z.boolean().default(true),scanClipboard:z.boolean().default(true),scanAI:z.boolean().default(true),timeoutMs:z.number().int().min(1000).max(60000).default(10000),maxBytes:z.number().int().min(1024).max(20*1024*1024).default(20*1024*1024),failClosed:z.literal(true).default(true)}).strict().prefault({})
}).strict().prefault({}).superRefine((p,c)=>{
if(p.dns.mode==='secure'&&!p.dns.templates.length)c.addIssue({code:'custom',path:['dns','templates'],message:'Secure DNS requires a corporate HTTPS resolver'});
if(p.dns.mode==='system'&&p.dns.templates.length)c.addIssue({code:'custom',path:['dns','templates'],message:'Clear resolver templates when using system DNS'});
if(p.vpn.required&&!p.vpn.interfaceNames.length)c.addIssue({code:'custom',path:['vpn','interfaceNames'],message:'Required VPN needs approved Windows interface names'});
if(p.icap.enabled&&!p.icap.url)c.addIssue({code:'custom',path:['icap','url'],message:'Enabled ICAP requires a TLS ICAP service URL'});
if(p.icap.enabled&&p.icap.url.startsWith('icap:')&&!p.icap.allowInsecureTransport&&!['127.0.0.1','[::1]','localhost'].includes(new URL(p.icap.url).hostname))c.addIssue({code:'custom',path:['icap','url'],message:'Remote plaintext ICAP requires explicit insecure transport approval'});
if(p.icap.enabled&&![p.icap.scanUploads,p.icap.scanDownloads,p.icap.scanClipboard,p.icap.scanAI].some(Boolean))c.addIssue({code:'custom',path:['icap'],message:'Select at least one inspection channel'});
});
export function enterpriseReadiness(p){return [
{id:'proxy',label:'Corporate proxy',state:p.proxy.mode==='direct'?'direct':'configured',detail:p.proxy.mode==='system'?'Uses Windows proxy and existing PAC configuration.':p.proxy.mode==='direct'?'Direct network access explicitly selected.':'Centrally managed proxy configuration.'},
{id:'dns',label:'Corporate DNS',state:p.enterprise.dns.mode==='secure'?'configured':'system',detail:p.enterprise.dns.mode==='secure'?'Required HTTPS DNS resolver; no insecure fallback.':'Uses OS DNS, including VPN-provided servers.'},
{id:'vpn',label:'VPN presence gate',state:p.enterprise.vpn.required?'configured':'optional',detail:'Windows interface presence is a gate, not proof of VPN routing or tunnel health.'},
{id:'icap',label:'External antivirus / sandbox',state:p.enterprise.icap.enabled?'configured':'off',detail:'ICAP request/response scanning, full payload, allow or block only. No content modification or deferred verdict support; unavailable inspection blocks selected channels.'},
{id:'downloads',label:'Download release',state:p.browser.allowDownloads&&((p.security.yara.enabled&&p.security.yara.rules.length)||(p.enterprise.icap.enabled&&p.enterprise.icap.scanDownloads))?'configured':'blocked',detail:'Downloads require configured local YARA or external ICAP scanning; enabled scanners are mandatory.'},
{id:'semantic',label:'Local semantic DLP',state:p.dlp.semantic.enabled?'configured':'off',detail:'Requires installed pinned model and calibrated references. Configuration does not prove device readiness.'}
];}
+29
View File
@@ -0,0 +1,29 @@
'use strict';
const {createHash}=require('node:crypto');
const path=require('node:path');
const TYPES=['pe','elf','macho','pdf','zip','png','jpeg','gif','webp','gzip','text','unknown'];
const EXT={exe:'pe',dll:'pe',sys:'pe',elf:'elf',pdf:'pdf',zip:'zip',docx:'zip',xlsx:'zip',pptx:'zip',png:'png',jpg:'jpeg',jpeg:'jpeg',gif:'gif',webp:'webp',gz:'gzip',gzip:'gzip',txt:'text',csv:'text',json:'text',html:'text',js:'text',css:'text',xml:'text',md:'text'};
const MIME={'application/pdf':'pdf','application/zip':'zip','application/x-zip-compressed':'zip','application/vnd.openxmlformats-officedocument.wordprocessingml.document':'zip','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':'zip','application/vnd.openxmlformats-officedocument.presentationml.presentation':'zip','image/png':'png','image/jpeg':'jpeg','image/gif':'gif','image/webp':'webp','application/gzip':'gzip','application/x-gzip':'gzip','application/json':'text','application/xml':'text','application/javascript':'text','application/x-msdownload':'pe','application/vnd.microsoft.portable-executable':'pe'};
function safeFilename(value){const name=path.win32.basename(path.posix.basename(String(value||'download.bin'))).replace(/[<>:"/\\|?*\x00-\x1f\x7f]/g,'_').replace(/[. ]+$/g,'').slice(0,180);return !name||/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name)?'download.bin':name;}
function detectType(value){
const b=Buffer.from(value);const starts=(hex)=>b.length>=hex.length/2&&b.subarray(0,hex.length/2).equals(Buffer.from(hex,'hex'));
// MZ is blocked as PE-family even if the header is corrupt or deliberately truncated.
if(starts('4d5a'))return 'pe';if(starts('7f454c46'))return 'elf';
if(['feedface','cefaedfe','feedfacf','cffaedfe','cafebabe','bebafeca','cafebabf','bfbafeca'].some(starts))return 'macho';
if(starts('255044462d'))return 'pdf';if(['504b0304','504b0506','504b0708'].some(starts))return 'zip';
if(starts('89504e470d0a1a0a'))return 'png';if(starts('ffd8ff'))return 'jpeg';
if(b.subarray(0,6).toString('ascii')==='GIF87a'||b.subarray(0,6).toString('ascii')==='GIF89a')return 'gif';
if(b.length>=12&&b.subarray(0,4).toString('ascii')==='RIFF'&&b.subarray(8,12).toString('ascii')==='WEBP')return 'webp';if(starts('1f8b'))return 'gzip';
// Decode the entire bounded payload. Prefix-only heuristics miss binary tails.
try{const text=new TextDecoder('utf-8',{fatal:true}).decode(b);if(!/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text))return 'text';}catch{}
return 'unknown';
}
function inspectFileTransfer(bytes,policy,direction,metadata={}){
if(!['upload','download'].includes(direction))throw Error('Unsupported transfer direction');
const settings=policy.fileTransfers?.[direction]||{};const name=safeFilename(metadata.filename),mime=String(metadata.mime||'').split(';',1)[0].trim().toLowerCase().slice(0,160);
const detectedType=detectType(bytes),extensionType=EXT[path.extname(name).slice(1).toLowerCase()]||null,mimeType=mime.startsWith('text/')?'text':MIME[mime]||null;
const facts={filename:name,mimeclaimed:mime,detectedType,size:bytes.length,sha256:createHash('sha256').update(bytes).digest('hex'),extensionMismatch:!!extensionType&&extensionType!==detectedType,mimeMismatch:!!mimeType&&mimeType!==detectedType};
let ruleId=null;if(bytes.length>(settings.maxBytes??20*1024*1024))ruleId='file-transfer-size-limit';else if(settings.blockedTypes?.includes(detectedType))ruleId='file-transfer-type-blocked';else if(settings.allowTypes?.length&&!settings.allowTypes.includes(detectedType))ruleId='file-transfer-type-not-allowed';else if(settings.blockUnknown&&detectedType==='unknown')ruleId='file-transfer-unknown';else if(settings.blockExtensionMismatch&&facts.extensionMismatch)ruleId='file-transfer-extension-mismatch';else if(settings.blockMimeMismatch&&facts.mimeMismatch)ruleId='file-transfer-mime-mismatch';
return {decision:ruleId?'block':'allow',ruleId,...facts};
}
module.exports={TYPES,safeFilename,detectType,inspectFileTransfer};
+5
View File
@@ -0,0 +1,5 @@
import {z} from 'zod';
import transfer from './file-transfer.cjs';
export const {TYPES,safeFilename,detectType,inspectFileTransfer}=transfer;
const DirectionSchema=z.object({maxBytes:z.number().int().min(1).max(20*1024*1024).default(20*1024*1024),allowTypes:z.array(z.enum(TYPES)).max(TYPES.length).default([]),blockedTypes:z.array(z.enum(TYPES)).max(TYPES.length).default([]),blockUnknown:z.boolean().default(false),blockExtensionMismatch:z.boolean().default(false),blockMimeMismatch:z.boolean().default(false),allowWithoutMalwareScanner:z.boolean().default(false)}).strict();
export const FileTransfersSchema=z.object({upload:DirectionSchema.prefault({}),download:DirectionSchema.prefault({})}).strict();
+22
View File
@@ -0,0 +1,22 @@
import { z } from 'zod';
// These names are shared by native IPC guards and the trusted browser chrome.
// Hiding a section removes access, rather than only changing its presentation.
export const SETTINGS_SECTIONS = Object.freeze(['appearance', 'library', 'ai', 'passwords', 'documentScan', 'agents']);
const sections = z.array(z.enum(SETTINGS_SECTIONS)).max(SETTINGS_SECTIONS.length).refine(items => new Set(items).size === items.length, 'Settings sections must be unique');
export const ManagedSettingsSchema = z.object({
visible: z.boolean().default(true),
hiddenSections: sections.default([]),
lockedSections: sections.default([]),
appearance: z.object({
mode: z.enum(['system', 'light', 'dark']).default('system'),
accent: z.enum(['blue', 'teal', 'violet']).default('blue'),
tabMode: z.enum(['separate', 'compact']).default('separate')
}).strict().prefault({})
}).strict().prefault({});
export function settingsSectionAllowed(policy, section) {
if (!SETTINGS_SECTIONS.includes(section)) return false;
const settings = ManagedSettingsSchema.safeParse(policy?.browser?.settings);
return settings.success && settings.data.visible && !settings.data.hiddenSections.includes(section) && !settings.data.lockedSections.includes(section);
}
+35
View File
@@ -0,0 +1,35 @@
'use strict';
// Application-layer policy encryption over authenticated HTTPS. No TLS replacement.
const c=require('node:crypto');
const VERSION='safetynet-policy-mlkem1024-mldsa87-v1';
const MAX=3*1024*1024;
const fail=()=>{throw Error('Protected management channel refused');};
const record=value=>{if(!value||typeof value!=='object'||Array.isArray(value))fail();return value;};
function exact(value,keys){record(value);if(Object.keys(value).sort().join(',')!==keys.slice().sort().join(','))fail();}
const encode=value=>value.toString('base64url');
function decode(value,max=MAX){if(typeof value!=='string'||value.length>Math.ceil(max*4/3)||!value.length||!/^[A-Za-z0-9_-]+$/.test(value))fail();const bytes=Buffer.from(value,'base64url');if(bytes.length>max||encode(bytes)!==value)fail();return bytes;}
function publicKey(pem,type){if(typeof pem!=='string'||pem.length>20000)fail();const key=c.createPublicKey(pem);if(key.asymmetricKeyType!==type)fail();return key;}
const bytes=value=>Buffer.from(JSON.stringify(value));
function signed(payload,edPrivate,pqPrivate){const data=bytes(payload);return {...payload,edSignature:encode(c.sign(null,data,edPrivate)),pqSignature:encode(c.sign(null,data,pqPrivate))};}
function verified(value,keys,edPublic,pqPublic){exact(value,[...keys,'edSignature','pqSignature']);const payload=Object.fromEntries(keys.map(key=>[key,value[key]]));const data=bytes(payload);if(!c.verify(null,data,publicKey(edPublic,'ed25519'),decode(value.edSignature,64))||!c.verify(null,data,publicKey(pqPublic,'ml-dsa-87'),decode(value.pqSignature,5000)))fail();return payload;}
function identity(edPrivate,pqPrivate,pqPublic){publicKey(pqPublic,'ml-dsa-87');return signed({version:VERSION,signatureKey:pqPublic},edPrivate,pqPrivate);}
function verifyIdentity(value,edPublic,pin){record(value);if(value.version!==VERSION||pin&&value.signatureKey!==pin)fail();verified(value,['version','signatureKey'],edPublic,pin||value.signatureKey);return value.signatureKey;}
function challenge(deviceId,edPrivate,pqPrivate,now=Date.now()){
const pair=c.generateKeyPairSync('ml-kem-1024');
const bundle=signed({version:VERSION,deviceId,id:encode(c.randomBytes(32)),expiresAt:now+60000,kemKey:pair.publicKey.export({type:'spki',format:'pem'})},edPrivate,pqPrivate);
return {bundle,privateKey:pair.privateKey};
}
function verifyChallenge(value,deviceId,edPublic,pqPublic,now=Date.now()){
const result=verified(value,['version','deviceId','id','expiresAt','kemKey'],edPublic,pqPublic);
if(result.version!==VERSION||result.deviceId!==deviceId||decode(result.id,32).length!==32||!Number.isSafeInteger(result.expiresAt)||result.expiresAt<=now||result.expiresAt>now+65000)fail();
publicKey(result.kemKey,'ml-kem-1024');return result;
}
function derive(shared,bundle,direction){const salt=c.createHash('sha384').update(bytes(bundle)).digest();return Buffer.from(c.hkdfSync('sha384',shared,salt,Buffer.from(VERSION+':'+direction),32));}
const aad=(bundle,direction)=>bytes({version:VERSION,id:bundle.id,deviceId:bundle.deviceId,direction});
function seal(key,value,bundle,direction){const input=bytes(value);if(input.length>MAX)fail();const iv=c.randomBytes(12),cipher=c.createCipheriv('aes-256-gcm',key,iv);cipher.setAAD(aad(bundle,direction));try{return {iv:encode(iv),ciphertext:encode(Buffer.concat([cipher.update(input),cipher.final()])),tag:encode(cipher.getAuthTag())};}finally{input.fill(0);}}
function open(key,value,bundle,direction){exact(value,['iv','ciphertext','tag']);const iv=decode(value.iv,12),tag=decode(value.tag,16);if(iv.length!==12||tag.length!==16)fail();const cipher=c.createDecipheriv('aes-256-gcm',key,iv);cipher.setAAD(aad(bundle,direction));cipher.setAuthTag(tag);let plaintext;try{plaintext=Buffer.concat([cipher.update(decode(value.ciphertext)),cipher.final()]);return JSON.parse(plaintext.toString('utf8'));}finally{plaintext?.fill(0);}}
function prepare(bundle){const {sharedKey,ciphertext}=c.encapsulate(publicKey(bundle.kemKey,'ml-kem-1024'));const requestKey=derive(sharedKey,bundle,'request'),responseKey=derive(sharedKey,bundle,'response');sharedKey.fill(0);const requestId=encode(c.randomBytes(32));try{return {request:{version:VERSION,id:bundle.id,deviceId:bundle.deviceId,kemCiphertext:encode(ciphertext),encrypted:seal(requestKey,{purpose:'policy',requestId},bundle,'request')},responseKey,requestId};}finally{requestKey.fill(0);}}
function receive(request,bundle,privateKey){exact(request,['version','id','deviceId','kemCiphertext','encrypted']);if(request.version!==VERSION||request.id!==bundle.id||request.deviceId!==bundle.deviceId||bundle.expiresAt<=Date.now())fail();const ciphertext=decode(request.kemCiphertext,1568);if(ciphertext.length!==1568)fail();const shared=c.decapsulate(privateKey,ciphertext),requestKey=derive(shared,bundle,'request'),responseKey=derive(shared,bundle,'response');shared.fill(0);try{const body=open(requestKey,request.encrypted,bundle,'request');exact(body,['purpose','requestId']);if(body.purpose!=='policy'||decode(body.requestId,32).length!==32)fail();return {responseKey,requestId:body.requestId};}catch(error){responseKey.fill(0);throw error;}finally{requestKey.fill(0);}}
function response(policy,bundle,context){return {version:VERSION,id:bundle.id,encrypted:seal(context.responseKey,{requestId:context.requestId,policy},bundle,'response')};}
function finish(value,bundle,context){exact(value,['version','id','encrypted']);if(value.version!==VERSION||value.id!==bundle.id)fail();const body=open(context.responseKey,value.encrypted,bundle,'response');exact(body,['requestId','policy']);if(body.requestId!==context.requestId)fail();return body.policy;}
module.exports={VERSION,identity,verifyIdentity,challenge,verifyChallenge,prepare,receive,response,finish};
+48
View File
@@ -0,0 +1,48 @@
import {z} from 'zod';
import {SETTINGS_SECTIONS} from './managed-settings.mjs';
import {BookmarkItemsSchema} from './bookmarks.mjs';
const id=z.string().min(1).max(80).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
const unique=(schema,max)=>z.array(schema).max(max).refine(items=>new Set(items).size===items.length,'Values must be unique');
const host=z.string().max(253).regex(/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i);
export const GroupRestrictionsSchema=z.object({
blockedHosts:unique(host,200).default([]),blockedPhrases:unique(z.string().min(1).max(4096),500).default([]),
disableDownloads:z.boolean().default(false),disableAI:z.boolean().default(false),disableAgents:z.boolean().default(false),
hideSettings:z.boolean().default(false),requireDocumentScan:z.boolean().default(false),
hiddenSections:unique(z.enum(SETTINGS_SECTIONS),SETTINGS_SECTIONS.length).default([]),
lockedSections:unique(z.enum(SETTINGS_SECTIONS),SETTINGS_SECTIONS.length).default([]),
managedBookmarks:BookmarkItemsSchema.default([])
}).strict();
export const PolicyGroupSchema=z.object({id,name:z.string().min(1).max(100).refine(v=>!/[\u0000-\u001f\u007f]/.test(v)),
memberDeviceIds:unique(id,10000),restrictions:GroupRestrictionsSchema,
rollout:z.object({stage:z.enum(['pilot','expanded','all']),deviceIds:unique(id,10000).default([])}).strict()
}).strict().superRefine((group,ctx)=>{
if(group.rollout.stage==='all'&&group.rollout.deviceIds.length)ctx.addIssue({code:'custom',message:'All-members rollout requires empty cohort'});
if(group.rollout.stage!=='all'&&(!group.rollout.deviceIds.length||group.rollout.deviceIds.some(i=>!group.memberDeviceIds.includes(i))))ctx.addIssue({code:'custom',message:'Pilot and expanded cohorts must be nonempty subsets of members'});
if(Buffer.byteLength(JSON.stringify(group))>250000)ctx.addIssue({code:'custom',message:'Group exceeds the 250 KB feature budget'});
});
export const GroupAssignmentSchema=z.object({deviceId:id,groups:unique(id,100),profiles:unique(id,100).optional()}).strict();
export function groupApplies(group,deviceId){return group.memberDeviceIds.includes(deviceId)&&(group.rollout.stage==='all'||group.rollout.deviceIds.includes(deviceId));}
// The company baseline always survives. Boolean restrictions combine with OR;
// block lists and hidden sections combine with a stable union, never replacement.
export function applyPolicyGroups(policy,groups,deviceId){
const result=structuredClone(policy),applied=groups.map(g=>PolicyGroupSchema.parse(g)).filter(g=>groupApplies(g,deviceId)).sort((a,b)=>a.id.localeCompare(b.id,'en'));
const union=(a,b)=>[...new Set([...a,...b])].sort();
for(const group of applied){const r=group.restrictions;
result.browser.blockedHosts=union(result.browser.blockedHosts,r.blockedHosts);
result.security.content.blockedPhrases=union(result.security.content.blockedPhrases,r.blockedPhrases);
if(r.disableDownloads)result.browser.allowDownloads=false;
if(r.disableAI)result.ai.enabled=false;
if(r.disableAgents)result.agents.enabled=false;
if(r.hideSettings)result.browser.settings.visible=false;
if(r.requireDocumentScan)result.inspection.documentAnalysisEnabled=true;
result.browser.settings.hiddenSections=union(result.browser.settings.hiddenSections,r.hiddenSections);
result.browser.settings.lockedSections=union(result.browser.settings.lockedSections,r.lockedSections);
for(const bookmark of r.managedBookmarks){const existing=result.browser.bookmarks.managed.find(b=>b.id===bookmark.id);
if(existing&&JSON.stringify(existing)!==JSON.stringify(bookmark))throw Error('Managed bookmark ID conflicts across policies');
if(!existing)result.browser.bookmarks.managed.push(bookmark);
}
}
result.assignment={deviceId,groups:applied.map(g=>g.id),...(result.assignment?.profiles?{profiles:result.assignment.profiles}:{})};
return result;
}
+133
View File
@@ -0,0 +1,133 @@
import { z } from 'zod';
import { createHash, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';
import { SecuritySchema, AgentsSchema } from './security.mjs';
import { SemanticPolicySchema } from './semantic.mjs';
import { ManagedSettingsSchema } from './managed-settings.mjs';
import { EnterpriseSchema } from './enterprise.mjs';
import { BookmarksSchema } from './bookmarks.mjs';
import {GroupAssignmentSchema} from './policy-groups.mjs';
import {DiagnosticsSchema} from './diagnostics.mjs';
import {FileTransfersSchema} from './file-transfer.mjs';
import {UserAccessSchema} from './user-profiles.mjs';
const host = z.string().max(253).regex(/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i);
export const PolicySchema = z.object({
version: z.number().int().positive(),
assignment:GroupAssignmentSchema.optional(),
userAccess:UserAccessSchema,
diagnostics:DiagnosticsSchema.prefault({}),
fileTransfers:FileTransfersSchema.prefault({}),
expiresAt: z.string().datetime(),
brand: z.object({ name: z.string().min(1).max(40), color: z.string().regex(/^#[0-9a-f]{6}$/i) }),
browser: z.object({
homeUrl: z.string().url(), allowedHosts: z.array(host).max(200), blockedHosts: z.array(host).max(200),
allowDownloads: z.boolean(), allowDevTools: z.boolean(), allowWebSockets: z.boolean(),
settings: ManagedSettingsSchema, bookmarks: BookmarksSchema
}),
proxy: z.discriminatedUnion('mode', [
z.object({ mode: z.literal('system') }), z.object({ mode: z.literal('direct') }),
z.object({ mode: z.literal('fixed_servers'), rules: z.string().min(1).max(2048), bypass: z.string().max(1024).default('') }),
z.object({ mode: z.literal('pac_script'), pacUrl: z.string().url().refine(v => v.startsWith('https://')) })
]),
dlp: z.object({
maxBytes: z.number().int().min(1024).max(10 * 1024 * 1024),
blockUninspectable: z.literal(true),
semantic: SemanticPolicySchema,
rules: z.array(z.object({ id: z.string().min(1).max(80), label: z.string().min(1).max(100),
kind: z.enum(['literal', 'sha256','fragment-sha256']), value: z.string().min(1).max(4096),
action: z.literal('block') }).superRefine((r,ctx) => {
if(r.kind !== 'literal' && !/^[a-f0-9]{64}$/i.test(r.value)) ctx.addIssue({code:'custom',message:'Invalid SHA-256 fingerprint'});
})).max(10000)
}),
security: SecuritySchema,
inspection:z.object({documentAnalysisEnabled:z.boolean().default(true)}).prefault({}),
passwordManager:z.object({enabled:z.boolean().default(true),allowSaving:z.boolean().default(true),allowAutofill:z.boolean().default(true),idleLockSeconds:z.number().int().min(60).max(900).default(300)}).strict().prefault({}),
agents: AgentsSchema,
enterprise: EnterpriseSchema,
ai: z.object({ enabled: z.boolean(), model: z.string().max(100),provider:z.enum(['openai-compatible','ollama']).default('openai-compatible'),trustMode:z.enum(['external','local']).default('external'),endpointId:z.string().max(80).default('default'),allowConfidentialLocal:z.boolean().default(false) })
}).strict().superRefine((p, ctx) => {
if (!['https:', 'http:'].includes(new URL(p.browser.homeUrl).protocol)) ctx.addIssue({code:'custom',message:'Home must be HTTP(S)'});
if(new Set(p.dlp.rules.map(r=>r.id)).size !== p.dlp.rules.length) ctx.addIssue({code:'custom',message:'Rule IDs must be unique'});
if(p.dlp.semantic.enabled&&(!p.dlp.semantic.revision||!p.dlp.semantic.categories.length))ctx.addIssue({code:'custom',message:'Semantic enforcement requires a pinned model revision and references'});
if(new Set(p.dlp.semantic.categories.map(r=>r.id)).size!==p.dlp.semantic.categories.length)ctx.addIssue({code:'custom',message:'Semantic category IDs must be unique'});
for(const [name,rules] of [['YARA',p.security.yara.rules],['Sigma',p.security.sigma.rules]])if(new Set(rules.map(r=>r.id)).size!==rules.length)ctx.addIssue({code:'custom',message:`${name} rule IDs must be unique`});
if(new Set(p.security.visualPhishing.brands.map(b=>b.id)).size!==p.security.visualPhishing.brands.length)ctx.addIssue({code:'custom',message:'Visual brand IDs must be unique'});
if(p.security.visualPhishing.enabled&&!p.security.visualPhishing.brands.length)ctx.addIssue({code:'custom',message:'Visual protection requires explicit reference brands'});
if(Buffer.byteLength(JSON.stringify(p))>1572864)ctx.addIssue({code:'custom',message:'Signed policy exceeds the 1.5 MiB feature budget'});
});
export function defaultPolicy() {
return PolicySchema.parse({ version: 1, expiresAt: new Date(Date.now()+7*86400000).toISOString(),
brand: {name:'SafetyNet',color:'#137c66'},
browser:{homeUrl:'https://example.com',allowedHosts:[],blockedHosts:[],allowDownloads:false,allowDevTools:false,allowWebSockets:false},
proxy:{mode:'system'}, dlp:{maxBytes:1048576,blockUninspectable:true,rules:[]}, ai:{enabled:false,model:''} });
}
export function hostMatches(hostname, pattern) {
const name = hostname.toLowerCase().replace(/\.$/, '');
const p = pattern.toLowerCase();
return p.startsWith('*.') ? name.endsWith(p.slice(1)) && name !== p.slice(2) : name === p;
}
export function destinationAllowed(url, policy) {
try {
const u = new URL(url);
if(!['http:','https:','ws:','wss:'].includes(u.protocol)) return false;
if(u.username || u.password) return false;
if(policy.browser.blockedHosts.some(p=>hostMatches(u.hostname,p))) return false;
if(policy.security?.dns?.enabled && policy.security.dns.blockedHosts.some(p=>hostMatches(u.hostname,p))) return false;
return !policy.browser.allowedHosts.length || policy.browser.allowedHosts.some(p=>hostMatches(u.hostname,p));
} catch { return false; }
}
const builtins = [
['private-key', /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/],
['aws-access-key', /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/],
['api-secret', /\b(?:sk-[a-zA-Z0-9_-]{20,}|gh[pousr]_[a-zA-Z0-9]{20,})\b/]
];
export function fragmentHashes(text){
const normalized=text.normalize('NFKC').toLowerCase().replace(/ß/g,'ss').replace(/ς/g,'σ'),words=normalized.match(/[\p{L}\p{N}_]+/gu)||[],hashes=new Set();
const hash=s=>createHash('sha256').update(s).digest('hex');for(let i=0;i+8<=words.length;i++)hashes.add(hash(words.slice(i,i+8).join(' ')));
for(const part of normalized.split(/\n\s*\n/)){const p=part.trim().replace(/\s+/gu,' ');if([...p].length>=32)hashes.add(hash(p));}return hashes;
}
export function inspect(bytes, policy) {
const data = Buffer.from(bytes);
if(data.length > policy.dlp.maxBytes) return {decision:'block',ruleId:'size-limit'};
const hash = createHash('sha256').update(data).digest('hex');
for(const r of policy.dlp.rules) if(r.kind==='sha256' && r.value.toLowerCase()===hash) return {decision:'block',ruleId:r.id};
let text;
try { text = new TextDecoder('utf-8',{fatal:true}).decode(data); }
catch { return {decision:'block',ruleId:'uninspectable'}; }
if(text.includes('\0')) return {decision:'block',ruleId:'uninspectable'};
const variants = [text];
try { variants.push(decodeURIComponent(text.replace(/\+/g,' '))); } catch {}
try { variants.push(JSON.stringify(JSON.parse(text))); } catch {}
for(const value of variants) {
let fragments;for(const r of policy.dlp.rules)if(r.kind==='fragment-sha256'){fragments??=fragmentHashes(value);if(fragments.has(r.value.toLowerCase()))return {decision:'block',ruleId:r.id};}
for(const phrase of policy.security?.content?.blockedPhrases||[])if(value.normalize('NFKC').toLowerCase().includes(phrase.normalize('NFKC').toLowerCase()))return {decision:'block',ruleId:'content-blocklist'};
for(const [id,pattern] of builtins) if(pattern.test(value)) return {decision:'block',ruleId:id};
for(const r of policy.dlp.rules) if(r.kind==='literal' && value.normalize('NFKC').toLowerCase().includes(r.value.normalize('NFKC').toLowerCase())) return {decision:'block',ruleId:r.id};
}
return {decision:'allow',ruleId:null};
}
export function allowsConfidentialLocal(verdict,policy){
if(verdict.decision!=='block'||!policy.ai.enabled||policy.ai.trustMode!=='local'||!policy.ai.allowConfidentialLocal)return false;
const id=verdict.ruleId;
if(!id||['private-key','aws-access-key','api-secret','content-blocklist','size-limit','uninspectable'].includes(id)||/yara|malware|parser|coverage|failure|failed|timeout|unsupported|encrypted|inspection|incomplete/i.test(id)||verdict.malware||policy.security?.yara?.rules.some(r=>r.id===id))return false;
return policy.dlp.rules.some(r=>r.id===id&&['literal','sha256','fragment-sha256'].includes(r.kind))||policy.dlp.semantic.categories.some(r=>r.id===id);
}
export function inspectForAI(bytes,policy){
const verdict=inspect(bytes,policy);if(!allowsConfidentialLocal(verdict,policy))return verdict;
// Re-scan without corporate detectors so a preceding phrase cannot hide a credential or content block.
const baseline=inspect(bytes,{...policy,dlp:{...policy.dlp,rules:[]}});if(baseline.decision!=='allow')return baseline;
return {...verdict,decision:'allow',classification:'local-only'};
}
export function signPolicy(policy, privatePem) {
const payload = Buffer.from(JSON.stringify(PolicySchema.parse(policy))).toString('base64url');
return {payload,signature:sign(null,Buffer.from(payload),createPrivateKey(privatePem)).toString('base64url')};
}
export function verifyPolicy(envelope, publicPem, minimumVersion=0) {
if(!verify(null,Buffer.from(envelope.payload),createPublicKey(publicPem),Buffer.from(envelope.signature,'base64url'))) throw Error('Policy signature rejected');
const policy = PolicySchema.parse(JSON.parse(Buffer.from(envelope.payload,'base64url').toString()));
if(policy.version < minimumVersion) throw Error('Policy rollback rejected');
if(Date.parse(policy.expiresAt)<=Date.now()) throw Error('Policy expired');
return policy;
}
+21
View File
@@ -0,0 +1,21 @@
import { PolicySchema } from './policy.mjs';
import { enterpriseReadiness } from './enterprise.mjs';
export const policyPresets=[
{id:'baseline',name:'Enterprise baseline',description:'Enforce clipboard protections, disable developer tools and unsupported WebSockets, retain existing network and DLP rules.'},
{id:'managed-ai',name:'Managed AI workspace',description:'Preserve approved AI access while restricting agent access to sensitive content and interaction scopes. Requires a configured approved gateway; add verified semantic references separately if needed. This preset does not enable an unconfigured AI provider.'},
{id:'restricted',name:'Restricted information',description:'Block downloads and local agents, prevent clipboard export, and hide employee settings. Existing content rules and network configuration remain in place.'},
{id:'existing-stack',name:'Preserve existing security stack',description:'Preserve current proxy, VPN, DNS and inspection configuration. Use the integration forms to connect your corporate services.'}
];
export function previewPreset(input,id){
const preset=policyPresets.find(v=>v.id===id);if(!preset)throw new Error('Unknown enterprise preset');
const p=PolicySchema.parse(structuredClone(input)),before=structuredClone(p);
if(id!=='existing-stack'){
p.browser.allowDevTools=false;p.browser.allowWebSockets=false;p.security.clipboard.enabled=true;p.security.clipboard.blockSuspiciousCommands=true;
}
if(id==='managed-ai'){p.agents.allowSensitiveRead=false;p.agents.scopes=p.agents.scopes.filter(scope=>!['read','screenshot','type','click'].includes(scope));}
if(id==='restricted'){
p.browser.allowDownloads=false;p.agents.enabled=false;p.security.clipboard.allowCopy=false;p.security.clipboard.allowCut=false;p.browser.settings.visible=false;
}
const changes=[];const compare=(a,b,path='')=>{for(const key of Object.keys(b)){const next=path?`${path}.${key}`:key;if(b[key]&&typeof b[key]==='object'&&!Array.isArray(b[key]))compare(a[key],b[key],next);else if(JSON.stringify(a[key])!==JSON.stringify(b[key]))changes.push({field:next,before:a[key],after:b[key]});}};compare(before,p);
return {preset,policy:PolicySchema.parse(p),changes,readiness:enterpriseReadiness(p),warning:'Draft preview only. Review deployment and explicitly publish the signed policy. No preset removes existing DLP rules, enables an AI provider, or supplies antivirus signatures.'};
}
+32
View File
@@ -0,0 +1,32 @@
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}));}
+19
View File
@@ -0,0 +1,19 @@
import { z } from 'zod';
export const SEMANTIC_MODEL='google/embeddinggemma-2';
export const SEMANTIC_DIMENSIONS=768;
export const SEMANTIC_PROFILE='document-v1';
export const SEMANTIC_ARTIFACT={artifactFormat:'gguf',quantization:'Q8_0',runtimeRevision:'b11461'};
export const ModelRevisionSchema=z.string().regex(/^[a-f0-9]{40}$/,'An exact lowercase model commit SHA is required');
export const EmbeddingVectorSchema=z.array(z.number().finite()).length(SEMANTIC_DIMENSIONS).refine(vector=>{const norm=vector.reduce((sum,v)=>sum+v*v,0);return Number.isFinite(norm)&&Math.abs(norm-1)<=0.002;},'Embedding must be finite, nonzero and normalized');
const CategorySchema=z.object({id:z.string().min(1).max(80),label:z.string().min(1).max(100),threshold:z.number().gt(0).max(1),prototype:EmbeddingVectorSchema,prototypes:z.array(EmbeddingVectorSchema).min(1).max(64).optional()}).strict();
export const defaultSemantic=()=>({enabled:false,model:SEMANTIC_MODEL,revision:'',dimensions:SEMANTIC_DIMENSIONS,profile:SEMANTIC_PROFILE,...SEMANTIC_ARTIFACT,categories:[]});
export const SemanticPolicySchema=z.preprocess(value=>{
// Only inactive legacy configuration is migrated. Old references cannot be assigned a new profile.
if(value&&typeof value==='object'&&value.enabled===false&&value.model===SEMANTIC_MODEL&&(value.profile===undefined||value.dimensions===undefined||value.artifactFormat===undefined))return defaultSemantic();
return value;
},z.object({enabled:z.boolean(),model:z.literal(SEMANTIC_MODEL),revision:z.union([z.literal(''),ModelRevisionSchema]),dimensions:z.literal(SEMANTIC_DIMENSIONS),profile:z.literal(SEMANTIC_PROFILE),artifactFormat:z.literal('gguf'),quantization:z.literal('Q8_0'),runtimeRevision:z.literal('b11461'),categories:z.array(CategorySchema).max(100)}).strict().superRefine((value,ctx)=>{
if((value.enabled||value.categories.length)&&!value.revision)ctx.addIssue({code:'custom',message:'Semantic references require a pinned model revision'});
if(value.enabled&&!value.categories.length)ctx.addIssue({code:'custom',message:'Semantic enforcement requires reference categories'});
if(value.categories.reduce((sum,category)=>sum+(category.prototypes?.length||1),0)>256)ctx.addIssue({code:'custom',message:'Semantic policy supports at most 256 reference vectors'});
})).default(defaultSemantic);
export const EmbeddingReferenceSchema=z.object({model:z.literal(SEMANTIC_MODEL),revision:ModelRevisionSchema,dimensions:z.literal(SEMANTIC_DIMENSIONS),profile:z.literal(SEMANTIC_PROFILE),artifactFormat:z.literal('gguf'),quantization:z.literal('Q8_0'),runtimeRevision:z.literal('b11461'),local:z.literal(true),backend:z.literal('native-gguf').optional(),device:z.literal('cpu').optional(),modalities:z.array(z.enum(['text','image','audio'])).min(1).max(3).optional(),vector:EmbeddingVectorSchema,vectors:z.array(EmbeddingVectorSchema).min(1).max(64),coverage:z.object({complete:z.boolean(),textChunks:z.number().int().min(0).max(64),extractionComplete:z.boolean(),mediaItems:z.number().int().min(0).max(64).optional()}).strict()}).strict().refine(value=>value.coverage.mediaItems?value.coverage.textChunks+value.coverage.mediaItems<=value.vectors.length:value.coverage.textChunks===value.vectors.length,'Chunk count must match supplied vectors');
+38
View File
@@ -0,0 +1,38 @@
import transferFacts from './transfer-facts.cjs';
const facts=metadata=>{const parsed=transferFacts.FileFactsSchema.safeParse(metadata.file);return parsed.success?parsed.data:null;};
export function toECS(event,installationId='safetynet') {
const m=event.metadata,block=event.kind.includes('block')||event.kind.endsWith('.denied')||m.decision==='block',failure=event.kind.endsWith('.failure'),alert=event.kind==='sigma.alert';
const config=event.kind.startsWith('policy.')||event.kind.startsWith('enrollment.')||event.kind==='device.revoked';
const auth=event.kind.startsWith('sso.'),clipboard=event.kind.startsWith('clipboard.');
return {
'@timestamp':event.time,ecs:{version:'8.17.0'},
event:{id:`${installationId}:${event.id}`,kind:alert?'alert':'event',module:'safetynet',dataset:'safetynet.security',
action:event.kind,category:[alert?'intrusion_detection':auth?'authentication':config?'configuration':clipboard?'process':'web'],
type:[alert?'info':failure?'error':auth?'start':config?'change':block?'denied':clipboard?'access':'allowed'],outcome:failure?'failure':'success',...(alert?{severity:severity(m.level)}:{})},
observer:{vendor:'SafetyNet',product:'Managed Enterprise Browser',type:'dlp',version:'0.1.0'},
...(event.device_id?{host:{id:event.device_id}}:{}),
...(m.actor?{user:{id:m.actor}}:{}),...(m.host?{destination:{domain:m.host}}:{}),
...(m.ruleId?{rule:{id:m.ruleId}}:{}),
...(facts(m)?{file:{size:facts(m).bytes,type:facts(m).detectedType,hash:{sha256:facts(m).sha256}}}:{}),
...(/^[a-f0-9]{32}$/.test(m.traceId||'')?{trace:{id:m.traceId}}:{}),
...(/^[a-f0-9]{16}$/.test(m.spanId||'')?{span:{id:m.spanId}}:{}),
safetynet:{policy_version:m.policyVersion??m.version??null,decision:m.decision??(block?'block':null),
content_type:m.contentType??null,model:m.model??null,coverage:m.coverage??null,classification:m.classification==='local-only'?'local-only':null,
...(alert?{source_event_id:`${installationId}:${m.sourceEventId}`,severity:m.level}:{})}
};
}
const header=v=>String(v).replace(/\\/g,'\\\\').replace(/\|/g,'\\|').replace(/[\r\n]/g,' ');
const ext=v=>String(v).replace(/\\/g,'\\\\').replace(/=/g,'\\=').replace(/\r/g,'\\r').replace(/\n/g,'\\n');
const severity=level=>({low:3,medium:5,high:7,critical:10}[level]??3);
export function toCEF(event,installationId='safetynet') {
const m=event.metadata,blocked=event.kind.includes('block')||event.kind.endsWith('.denied')||m.decision==='block',alert=event.kind==='sigma.alert';
const fields={externalId:`${installationId}:${event.id}`,rt:Date.parse(event.time),act:event.kind,
...(event.device_id?{deviceExternalId:event.device_id}:{}),...(m.actor?{suser:m.actor}:{}),
...(m.ruleId?{cs1Label:'RuleId',cs1:m.ruleId}:{}),...(m.host?{dhost:m.host}:{}),
...(m.policyVersion||m.version?{cn1Label:'PolicyVersion',cn1:m.policyVersion??m.version}:{}),
...(facts(m)?{cn2Label:'FileBytes',cn2:facts(m).bytes,cs6Label:'DetectedFileType',cs6:facts(m).detectedType,fileHash:facts(m).sha256}:{}),
...(/^[a-f0-9]{32}$/.test(m.traceId||'')?{cs4Label:'TraceId',cs4:m.traceId}:{}),
...(/^[a-f0-9]{16}$/.test(m.spanId||'')?{cs5Label:'SpanId',cs5:m.spanId}:{}),
...(alert?{cs2Label:'SourceEventId',cs2:`${installationId}:${m.sourceEventId}`,cs3Label:'AlertLevel',cs3:m.level}:{})};
return `CEF:0|SafetyNet|Enterprise Browser|0.1.0|${header(event.kind)}|${header(event.kind)}|${alert?severity(m.level):blocked?7:3}|`+Object.entries(fields).map(([k,v])=>`${k}=${ext(v)}`).join(' ');
}
+5
View File
@@ -0,0 +1,5 @@
'use strict';
const {z}=require('zod');
const FileFactsSchema=z.object({bytes:z.number().int().min(0).max(1073741824),detectedType:z.enum(['pe','elf','macho','pdf','zip','png','jpeg','gif','webp','gzip','text','unknown']),sha256:z.string().regex(/^[a-f0-9]{64}$/)}).strict();
function transferFacts(value){return value?FileFactsSchema.parse(value):undefined;}
module.exports={FileFactsSchema,transferFacts};
+27
View File
@@ -0,0 +1,27 @@
import {z} from 'zod';
const id=z.string().min(1).max(80).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
const unique=(schema,max)=>z.array(schema).max(max).refine(a=>new Set(a).size===a.length,'Duplicate values');
const host=z.string().max(253).regex(/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i);
export const UserAccessSchema=z.object({delegatableFeatures:z.object({devTools:z.boolean().default(false)}).strict().prefault({})}).strict().prefault({});
export const DomainCategorySchema=z.object({id,name:z.string().min(1).max(100),hosts:unique(host,500)}).strict();
export const UserProfileSchema=z.object({id,name:z.string().min(1).max(100),subjects:unique(z.string().min(1).max(512),10000).default([]),deviceIds:unique(id,10000).default([]),allowedHosts:unique(host,500).default([]),blockedHosts:unique(host,500).default([]),blockedCategoryIds:unique(id,100).default([]),features:z.object({devTools:z.enum(['inherit','allow','deny']).default('inherit'),downloads:z.enum(['inherit','deny']).default('inherit'),ai:z.enum(['inherit','deny']).default('inherit'),agents:z.enum(['inherit','deny']).default('inherit')}).strict().prefault({}),requireDocumentScan:z.boolean().default(false)}).strict();
export const UserProfileConfigurationSchema=z.object({profiles:z.array(UserProfileSchema).max(100).default([]),categories:z.array(DomainCategorySchema).max(100).default([])}).strict();
export function profileApplies(profile,device){return profile.deviceIds.includes(device.id)||!!device.verified_subject&&profile.subjects.includes(device.verified_subject);}
const covers=(a,b)=>a===b||a.startsWith('*.')&&b.endsWith(a.slice(1));
export function applyUserProfiles(policy,configuration,device){
const result=structuredClone(policy),config=UserProfileConfigurationSchema.parse(configuration||{}),profiles=config.profiles.filter(p=>profileApplies(p,device)).sort((a,b)=>a.id.localeCompare(b.id));
const union=(a,b)=>[...new Set([...a,...b])].sort();
for(const p of profiles){
result.browser.blockedHosts=union(result.browser.blockedHosts,p.blockedHosts);
for(const categoryId of p.blockedCategoryIds){const category=config.categories.find(c=>c.id===categoryId);if(!category)throw Error('Unknown domain category');result.browser.blockedHosts=union(result.browser.blockedHosts,category.hosts);}
if(p.allowedHosts.length){const existing=result.browser.allowedHosts;result.browser.allowedHosts=existing.length?union(existing.filter(a=>p.allowedHosts.some(b=>covers(b,a))),p.allowedHosts.filter(b=>existing.some(a=>covers(a,b)))):p.allowedHosts;if(!result.browser.allowedHosts.length){result.browser.allowedHosts=['denied.invalid'];result.browser.blockedHosts=union(result.browser.blockedHosts,['denied.invalid']);}}
if(p.features.downloads==='deny')result.browser.allowDownloads=false;
if(p.features.ai==='deny')result.ai.enabled=false;
if(p.features.agents==='deny')result.agents.enabled=false;
if(p.requireDocumentScan)result.inspection.documentAnalysisEnabled=true;
}
if(profiles.some(p=>p.features.devTools==='deny'))result.browser.allowDevTools=false;
else if(result.userAccess.delegatableFeatures.devTools&&profiles.some(p=>p.features.devTools==='allow'))result.browser.allowDevTools=true;
result.assignment={deviceId:device.id,groups:[],...(profiles.length?{profiles:profiles.map(p=>p.id)}:{})};
return result;
}
+73
View File
@@ -0,0 +1,73 @@
'use strict';
// A visual similarity feature, never an authentication or phishing verdict.
const SIZE = 32;
const MAX_PIXELS = 16 * 1024 * 1024;
const ALGORITHM = 'phash-dct32-v1';
const COSINES = Array.from({ length: 8 }, (_, frequency) =>
Float64Array.from({ length: SIZE }, (_, position) => Math.cos(Math.PI * (2 * position + 1) * frequency / (2 * SIZE))));
function invalid() { throw new Error('Invalid visual fingerprint input'); }
function visualFingerprint({ rgba, width, height, region } = {}) {
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > 8192 || height > 8192 || width * height > MAX_PIXELS) invalid();
if (!ArrayBuffer.isView(rgba) || rgba.BYTES_PER_ELEMENT !== 1 || rgba.byteLength !== width * height * 4) invalid();
if (typeof SharedArrayBuffer !== 'undefined' && rgba.buffer instanceof SharedArrayBuffer) invalid();
const pixels = new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength);
let left = 0, top = 0, right = width, bottom = height;
if (region !== undefined) {
if (!region || !['x', 'y', 'width', 'height'].every(key => Number.isInteger(region[key]) && Math.abs(region[key]) <= 32768) || region.width <= 0 || region.height <= 0) invalid();
left = Math.max(0, region.x); top = Math.max(0, region.y);
right = Math.min(width, region.x + region.width); bottom = Math.min(height, region.y + region.height);
if (left >= right || top >= bottom) invalid();
}
function luminance(x, y) {
const index = (y * width + x) * 4;
const alpha = pixels[index + 3] / 255;
return alpha * (0.299 * pixels[index] + 0.587 * pixels[index + 1] + 0.114 * pixels[index + 2]) + (1 - alpha) * 255;
}
const grid = new Float64Array(SIZE * SIZE);
// Centered bilinear resampling of the entire supplied crop preserves geometry.
for (let y = 0; y < SIZE; y++) {
const py = Math.max(top, Math.min(bottom - 1, top + (y + 0.5) * (bottom - top) / SIZE - 0.5));
const y0 = Math.floor(py), y1 = Math.min(bottom - 1, y0 + 1), dy = py - y0;
for (let x = 0; x < SIZE; x++) {
const px = Math.max(left, Math.min(right - 1, left + (x + 0.5) * (right - left) / SIZE - 0.5));
const x0 = Math.floor(px), x1 = Math.min(right - 1, x0 + 1), dx = px - x0;
grid[y * SIZE + x] = (1 - dy) * ((1 - dx) * luminance(x0, y0) + dx * luminance(x1, y0)) + dy * ((1 - dx) * luminance(x0, y1) + dx * luminance(x1, y1));
}
}
const mean = grid.reduce((sum, value) => sum + value, 0) / grid.length;
const stddev = Math.sqrt(grid.reduce((sum, value) => sum + (value - mean) ** 2, 0) / grid.length);
const histogram = new Uint16Array(16);
let minimum = 255, maximum = 0, detail = 0;
for (let y = 0; y < SIZE; y++) for (let x = 0; x < SIZE; x++) {
const value = grid[y * SIZE + x];
minimum = Math.min(minimum, value); maximum = Math.max(maximum, value);
histogram[Math.min(15, Math.floor(value / 16))]++;
if (x) detail += Math.abs(value - grid[y * SIZE + x - 1]);
if (y) detail += Math.abs(value - grid[(y - 1) * SIZE + x]);
}
detail /= 2 * SIZE * (SIZE - 1);
const entropy = histogram.reduce((sum, count) => { const probability = count / grid.length; return count ? sum - probability * Math.log2(probability) : sum; }, 0);
const coefficients = [];
// Orthonormal DCT-II, low-frequency 8x8 block. DC is deliberately excluded.
for (let v = 0; v < 8; v++) for (let u = 0; u < 8; u++) {
if (!u && !v) { coefficients.push(0); continue; }
let coefficient = 0;
for (let y = 0; y < SIZE; y++) for (let x = 0; x < SIZE; x++) coefficient += (grid[y * SIZE + x] - mean) * COSINES[u][x] * COSINES[v][y];
coefficient *= (u ? Math.sqrt(2 / SIZE) : Math.sqrt(1 / SIZE)) * (v ? Math.sqrt(2 / SIZE) : Math.sqrt(1 / SIZE));
coefficients.push(Math.round(coefficient * 1e8) / 1e8);
}
const sorted = coefficients.slice(1).sort((a, b) => a - b), median = sorted[31];
let bits = 0n;
for (let index = 1; index < coefficients.length; index++) if (coefficients[index] > median) bits |= 1n << BigInt(63 - index);
const round = value => Math.round(value * 10000) / 10000;
return { algorithm: ALGORITHM, hash: bits.toString(16).padStart(16, '0'),
quality: { contrast: round(stddev), stddev: round(stddev), dynamicRange: round(maximum - minimum), entropy: round(entropy), detail: round(detail) },
usable: stddev >= 10 && maximum - minimum >= 32 && entropy >= 0.5 && detail >= 1 };
}
function hammingDistance(left, right) {
if (typeof left !== 'string' || typeof right !== 'string' || !/^[a-f0-9]{16}$/.test(left) || !/^[a-f0-9]{16}$/.test(right)) invalid();
let different = BigInt('0x' + left) ^ BigInt('0x' + right), count = 0;
while (different) { different &= different - 1n; count++; }
return count;
}
module.exports = { visualFingerprint, hammingDistance };
+29
View File
@@ -0,0 +1,29 @@
'use strict';
const {PNG}=require('pngjs');
const {inflateSync}=require('node:zlib');
const {visualFingerprint}=require('./visual-hash.cjs');
const SIGNATURE=Buffer.from([137,80,78,71,13,10,26,10]);
function decodePNG(input){
const bytes=Buffer.from(input);
if(bytes.length<45||bytes.length>10*1024*1024||!bytes.subarray(0,8).equals(SIGNATURE)||bytes.readUInt32BE(8)!==13||bytes.toString('ascii',12,16)!=='IHDR')throw Error('Invalid bounded PNG reference');
const width=bytes.readUInt32BE(16),height=bytes.readUInt32BE(20),depth=bytes[24],type=bytes[25];
if(width<16||height<16||width>8192||height>8192||width*height>8*1024*1024||depth!==8||![0,2,4,6].includes(type)||bytes[26]!==0||bytes[27]!==0||bytes[28]!==0)throw Error('Unsupported PNG dimensions or encoding');
const parts=[];let ended=false;
for(let offset=8;offset<bytes.length;){
if(offset+12>bytes.length)throw Error('Truncated PNG');
const length=bytes.readUInt32BE(offset),chunk=bytes.toString('ascii',offset+4,offset+8),next=offset+12+length;
if(next>bytes.length||ended||(chunk==='IHDR'&&offset!==8)||['acTL','fcTL','fdAT'].includes(chunk))throw Error('Unsupported PNG structure');
if(chunk==='IDAT')parts.push(bytes.subarray(offset+8,offset+8+length));
if(chunk==='IEND'){if(length!==0||next!==bytes.length)throw Error('Invalid PNG end');ended=true;}
offset=next;
}
if(!ended||!parts.length)throw Error('Incomplete PNG');
const channels={0:1,2:3,4:2,6:4}[type],expected=(width*channels+1)*height;
const inflated=inflateSync(Buffer.concat(parts),{maxOutputLength:expected+1});
try{if(inflated.length!==expected)throw Error('PNG data size differs from header');}finally{inflated.fill(0);}
const image=PNG.sync.read(bytes,{checkCRC:true});
if(image.width!==width||image.height!==height||image.data.length!==width*height*4)throw Error('PNG output dimensions invalid');
return {rgba:image.data,width,height};
}
function fingerprintPNG(bytes){const image=decodePNG(bytes);try{return {...visualFingerprint(image),width:image.width,height:image.height};}finally{image.rgba.fill(0);}}
module.exports={decodePNG,fingerprintPNG};