const { app, BrowserWindow, WebContentsView, ipcMain, session, safeStorage, shell, dialog, powerMonitor, clipboard, Menu } = require('electron'); const {assertSecureStartup}=require('./startup-security.cjs'); const {assertPolicyAssignment}=require('./policy-assignment.cjs'); assertSecureStartup(app.commandLine); // Force Chromium sandboxing globally before any renderer is created. The subframe // preload option below never grants a Node engine: native proof tests verify this. app.enableSandbox(); const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const { pathToFileURL } = require('node:url'); const {createDiagnostics}=require('./diagnostics.cjs'); const diagnostics=createDiagnostics(); function diagnosticContext(extra={}){return {policyVersion:policy?.version||0,...(device?.id?{deviceId:device.id}:{}),...extra};} function diagnosticSpan(name,attributes,operation){ // Instrumentation never replaces, skips or retries a security operation. let started=false;const run=()=>{started=true;return Promise.resolve(operation()).then(result=>{if(['allow','block','deny','unknown'].includes(result?.decision)){try{diagnostics.event(name,diagnosticContext({...attributes,decision:result.decision}));}catch{}}return result;});}; try{return diagnostics.span(name,diagnosticContext(attributes),run);}catch(error){if(started)throw error;return run();} } let diagnosticsConfiguration=''; function configureDiagnostics(config){const value=config||{enabled:false},context=diagnosticContext(),key=JSON.stringify([value,context]);if(key===diagnosticsConfiguration)return;try{diagnostics.configure(value,context);diagnosticsConfiguration=key;}catch{diagnosticsConfiguration='';try{diagnostics.configure({enabled:false});}catch{}}} function diagnosticCorrelation(){try{const ids=diagnostics.correlation?.();return /^[a-f0-9]{32}$/.test(ids?.traceId||'')&&/[1-9a-f]/.test(ids.traceId)&&/^[a-f0-9]{16}$/.test(ids?.spanId||'')&&/[1-9a-f]/.test(ids.spanId)?{traceId:ids.traceId,spanId:ids.spanId}:{};}catch{return {};}} let win,view,browserSession,managementSession,core,device,policy,ready=false,syncing=false,aiPanel=false,vaultPanel=false,vault,vaultTimer; let chromeHeight=104,leftSidebar=false,startPage=false,overviewPanel=false,toolsOpen=false,pageTitle='',pageColor='#edf1f6'; const {createTabRegistry}=require('./tab-registry.cjs'); let tabs,tabsRevision=0; function activeTab(){return tabs?.active();} function tabContext(id,{foreground=true}={}){const tab=id?tabs.get(id):activeTab();if(!tab)throw Error('No native browser tab');return tabs.context(tab.id,{foreground});} function currentTabContext(token){return ready&&token?.policyVersion===policy?.version&&tabs.current(token)&&Date.parse(policy?.expiresAt)>Date.now();} function selectTab(id){const target=tabs.get(id);if(!target.guarded)throw Error('Native tab security is still initializing');const changed=target!==activeTab();const tab=tabs.activate(id);if(changed){win.webContents.focus();vault?.lock();vaultPanel=false;}view=tab.view;startPage=tab.startPage;pageTitle=tab.title;pageColor=tab.color;layout();return tab;} function blockedTab(){return tabs?.all().find(tab=>tab.security.visualBlocked||tab.security.contentBlocked);} function stickyBlock(){const tab=blockedTab();return tab&&(tab.security.visualBlocked||tab.security.contentBlocked);} const {startInspector}=require('./inspection.cjs'); const {icapRequired,scanEnterprise,scanDownloadEnterprise}=require('./enterprise-inspection.cjs'); const {createDownloadGate}=require('./download-gate.cjs'); const {resolverOptions,createNetworkPosture}=require('./network-posture.cjs'); const networkPosture=createNetworkPosture();let networkTimer,networkChecking=false; const {startAgentControl}=require('./agent-control.cjs'); const {createAgentAdapters}=require('./agent-adapters.cjs'); const {createVault}=require('./vault.cjs'); const {createDeveloperToolsGuard,developerAssetRequestAllowed}=require('./developer-tools.cjs'); const {settingsState,sectionVisible,sectionAllowed,featureAllowed,requireFeature,requireSection,validateAppearance}=require('./managed-settings.cjs'); const {bookmarkState,validateMutation:validateBookmarkMutation,libraryVisible}=require('./bookmarks.cjs'); let bookmarkBusy=false,personalBookmarks={revision:0,items:[]},bookmarkSnapshotJSON=''; function effectiveBookmarkPolicy(){return policy?{...policy,browser:{...policy.browser,bookmarks:{...policy.browser.bookmarks,personal:personalBookmarks}}}:null;} let appearancePreferences={}; function managedSettings(){return settingsState({policy,ready,preferences:appearancePreferences});} function requireManagedSection(section){requireSection(managedSettings(),section);} function requireFeatureSection(section){requireFeature(managedSettings(),section);} function loadAppearance(){try{appearancePreferences=validateAppearance(JSON.parse(fs.readFileSync(path.join(app.getPath('userData'),'appearance.json'),'utf8')));}catch{appearancePreferences={};}} function persistAppearance(){fs.writeFileSync(path.join(app.getPath('userData'),'appearance.json'),JSON.stringify(appearancePreferences),{mode:0o600});} const {fillScript}=require('./password-fill.cjs'); const {createVisualPhishing,describeScript}=require('./visual-phishing.cjs'); const {fingerprintPNG}=require('../../packages/core/visual-image.cjs'); const {hammingDistance}=require('../../packages/core/visual-hash.cjs'); const {capturePNG}=require('./visual-capture.cjs'); const {createClipboardGuard}=require('./clipboard-guard.cjs'); const {clipboardScript}=require('./clipboard-dom.cjs'); const {attachPageClipboardGuard,disposePageClipboardGuard}=require('./page-clipboard-guard.cjs'); let clipboardGuard,clipboardCore,clipboardFatal=false; function installClipboardControls(contents){ contents.on('before-input-event',(event,input)=>{ if(input.type!=='keyDown')return; const key=input.key.toLowerCase(),modifier=(input.control||input.meta)&&!input.alt; const operation=modifier&&key==='c'||input.control&&key==='insert'?'copy':modifier&&key==='x'||input.shift&&key==='delete'?'cut':modifier&&key==='v'||input.shift&&key==='insert'?'paste':null; if(operation){event.preventDefault();diagnosticSpan('browser.clipboard',{operation},()=>clipboardGuard?.perform(operation,contents)).catch(()=>{});} }); contents.on('context-menu',(_event,params)=>{ const actions=params.isEditable?['copy','cut','paste']:params.selectionText?['copy']:[]; if(!actions.length)return; Menu.buildFromTemplate(actions.map(operation=>({label:operation[0].toUpperCase()+operation.slice(1),click:()=>diagnosticSpan('browser.clipboard',{operation},()=>clipboardGuard?.perform(operation,contents)).catch(()=>{})}))).popup({window:win}); }); } let managementHealthy=false; function visualDeadline(operation){let timeout;return Promise.race([operation,new Promise((_,reject)=>{timeout=setTimeout(()=>reject(Error('Visual check timed out')),5000);})]).finally(()=>clearTimeout(timeout));} function checkVisual(...args){return diagnosticSpan('browser.visual',args[0]?.id?{sessionId:args[0].id}:{},()=>checkVisualImpl(...args));} async function checkVisualImpl(tab=activeTab()){ if(!tab||tab.view.webContents.isDestroyed())return {decision:'unknown'}; if(!policy?.security.visualPhishing.enabled)return {decision:'allow'}; const security=tab.security; if(!security.visualPending){const operation=security.visualScanner.check({force:true});security.visualPending=operation;operation.finally(()=>{if(security.visualPending===operation)security.visualPending=null;}).catch(()=>{});} const generation=tab.navigationId,version=policy.version,url=tab.view.webContents.getURL(),result=await security.visualPending; if(ready&&generation===tab.navigationId&&version===policy.version&&!tab.view.webContents.isDestroyed()&&url===tab.view.webContents.getURL()&&result.decision==='allow')security.visualApproval={generation,version,url}; if(!tab.view.webContents.isDestroyed()&&generation===tab.navigationId&&version===policy.version&&url===tab.view.webContents.getURL()){if(ready){if(result.decision==='unknown')message='Login protection could not approve this view. Credential actions are held.';emit();}return result;} return {decision:'unknown',reason:'stale-context'}; } async function configureVisualPage(enabled,tab=activeTab()){ if(!tab)return;tabs.navigation(tab.id);const contents=tab.view.webContents; if(contents.isDestroyed()||!/^https?:\/\//.test(contents.getURL()))return; const token=require('node:crypto').randomUUID(); await new Promise((resolve,reject)=>{ const handler=(event,value)=>{if(event.sender===contents&&event.senderFrame===contents.mainFrame&&value===token){clearTimeout(timeout);ipcMain.removeListener('managed:visual-configured',handler);resolve();}}; const timeout=setTimeout(()=>{ipcMain.removeListener('managed:visual-configured',handler);enabled?reject(Error('Visual protection could not initialize')):resolve();},2000); ipcMain.on('managed:visual-configured',handler);contents.send('managed:visual-config',{enabled,token}); }); } let message='Enroll this browser to receive company policy.',syncTimer,localInspector,auditQueue=[],flushing=false,agentService,agentStarting=false,securityCore; const recentRequests=new Map(); const agentFile=()=>path.join(app.getPath('userData'),'agent-connection.json'); app.commandLine.appendSwitch('disable-http-cache'); const uiUrl=pathToFileURL(path.join(__dirname,'index.html')).href; const configPath=()=>path.join(app.getPath('userData'),'enrollment.json'); const cliRequest=app.commandLine.getSwitchValue('agent-action'),cliConnection=app.commandLine.getSwitchValue('agent-connection'); if(cliRequest){try{require('./agent-client.cjs').configureClient(cliConnection);}catch{app.exit(1);}} const ownsBrowserInstance=!!cliRequest||app.requestSingleInstanceLock(); if(!ownsBrowserInstance)app.quit(); else if(!cliRequest)app.on('second-instance',()=>{if(win&&!win.isDestroyed()){if(win.isMinimized())win.restore();win.focus();}}); function requireEncryption(){ if(!safeStorage.isEncryptionAvailable() || (process.platform==='linux' && safeStorage.getSelectedStorageBackend()==='basic_text'))throw Error('An OS credential store is required. Configure Keychain, Windows DPAPI, or Linux Secret Service.'); } function saveDevice(value){requireEncryption();fs.writeFileSync(configPath(),JSON.stringify({encryptedEnrollment:safeStorage.encryptString(JSON.stringify(value)).toString('base64')}),{mode:0o600});} function loadDevice(){ if(!fs.existsSync(configPath()))return; requireEncryption();const d=JSON.parse(fs.readFileSync(configPath(),'utf8'));return JSON.parse(safeStorage.decryptString(Buffer.from(d.encryptedEnrollment,'base64'))); } function managementUrl(value){ const u=new URL(value); if(u.username||u.password||u.search||u.hash||u.pathname!=='/')throw Error('Enter a management server origin without path or credentials'); if(u.protocol!=='https:' && !(u.protocol==='http:'&&['localhost','127.0.0.1','[::1]'].includes(u.hostname)))throw Error('Management requires HTTPS; local development may use loopback HTTP.'); if(process.env.SAFETYNET_MANAGEMENT_URL&&new URL(process.env.SAFETYNET_MANAGEMENT_URL).origin!==u.origin)throw Error('Enrollment must use the administrator-deployed management server'); return u.origin; } const managementRequests=new Map(); function request(...args){return diagnosticSpan('browser.management',{operation:args[1]==='/api/device/ai'?'ai':args[1]==='/api/device/enroll'?'enroll':'sync'},()=>requestImpl(...args));} async function requestImpl(server,route,body,credential){ if(route==='/api/device/policy'){ if(!device||device.server!==server||device.token!==credential)throw Error('Protected management identity unavailable'); return require('./management-channel.cjs').protectedPolicyRequest({device,request,save:saveDevice}); } const headers={};if(body)headers['content-type']='application/json';if(credential)headers.authorization=`Bearer ${credential}`; if(route==='/api/device/enroll'||route==='/api/device/sso/poll')headers['x-safetynet-pq-bootstrap']='1'; const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),route.endsWith('/ai')?65000:10000);managementRequests.set(controller,route); try{ const fetcher=managementSession?managementSession.fetch.bind(managementSession):fetch; const r=await fetcher(`${server}${route}`,{method:body?'POST':'GET',headers,body:body?JSON.stringify(body):undefined,credentials:'omit',redirect:'error',signal:controller.signal}); const chunks=[];let size=0;for await(const part of r.body){size+=part.length;if(size>5*1024*1024)throw Error('Management response exceeds its limit');chunks.push(part);} const data=JSON.parse(Buffer.concat(chunks).toString('utf8'));if(!r.ok && r.status!==202){const e=Error('Management request refused');e.status=r.status;throw e;}return data; }finally{clearTimeout(timer);managementRequests.delete(controller);} } function state(){return {bookmarks:bookmarkState({policy:effectiveBookmarkPolicy(),ready,settings:managedSettings()}),settings:managedSettings(),enrolled:!!device,deviceId:device?.id||'',ready,clipboardPending:clipboardGuard?.isPending()||false,...tabs?.summary(),tabsRevision:++tabsRevision,canNavigate:ready||!!(stickyBlock()&&managementHealthy),loginCheckPending:ready&&!!policy?.security.visualPhishing.enabled&&(!activeTab()?.security.visualApproval||activeTab().security.visualApproval.generation!==activeTab().navigationId),message,brand:policy?.brand||{name:'SafetyNet',color:'#137c66'},policyVersion:policy?.version||0,server:device?.server||'',url:view?.webContents.getURL()||'',aiEnabled:ready&&!!policy?.ai.enabled,documentScanEnabled:ready&&policy?.inspection?.documentAnalysisEnabled!==false,passwordManagerEnabled:ready&&!!policy?.passwordManager?.enabled,vaultLocked:vault?.status().locked??true,vaultExists:vault?.status().exists??fs.existsSync(path.join(app.getPath('userData'),'password-vault.json')),agentsEnabled:ready&&!!policy?.agents.enabled,agentConnected:!!agentService};} function emit(){if(win&&!win.isDestroyed()){const settings=managedSettings();if(!settings.visible)toolsOpen=false;if(!sectionAllowed(settings,'appearance'))chromeHeight=settings.appearance.tabMode==='compact'?64:104;if(!featureAllowed(settings,'ai'))aiPanel=false;if(!featureAllowed(settings,'passwords')){vaultPanel=false;vault?.lock();}if(!libraryVisible(settings))leftSidebar=false;layout();win.webContents.send('managed:state',{...state(),pageTitle,pageColor,windowMaximized:win.isMaximized()});win.setTitle(`S Browser · ${state().brand.name}`);}} function layout(){const tab=activeTab();if(!win||!tab)return;view=tab.view;startPage=tab.startPage;const [width,height]=win.getContentSize(),left=leftSidebar?240:0;view.setBounds({x:left,y:chromeHeight,width:Math.max(0,width-left-((aiPanel||vaultPanel)?360:0)),height:Math.max(0,height-chromeHeight)});view.setVisible(ready&&tab.guarded&&!startPage&&!overviewPanel&&!toolsOpen);} async function pageTheme(tab=activeTab()){ if(!tab||!ready)return;const contents=tab.view.webContents,url=contents.getURL(),context=tabContext(tab.id,{foreground:false});if(!/^https?:\/\//.test(url))return; try{const result=await contents.executeJavaScriptInIsolatedWorld(999,[{code:`(()=>{if(location.href!==${JSON.stringify(url)})return null;return {color:(document.querySelector('meta[name="theme-color"]')?.content||'').slice(0,32),background:getComputedStyle(document.body||document.documentElement).backgroundColor};})()`}],false); if(!currentTabContext(context)||!result)return;const hex=/^#[a-f0-9]{6}$/i.test(result.color)?result.color:null,rgb=/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(result.background||'');tab.color=hex||(rgb&&rgb.slice(1).every(v=>+v<=255)?'#'+rgb.slice(1).map(v=>(+v).toString(16).padStart(2,'0')).join(''):'#edf1f6');tab.title=contents.getTitle().slice(0,200);if(tab===activeTab()){pageColor=tab.color;pageTitle=tab.title;}emit(); }catch{} } function lockVault(){const unlocked=vault&&!vault.status().locked;vault?.lock();if(unlocked)audit('vault.locked');emit();} function quarantine(reason){ready=false;if(policy&&Date.parse(policy.expiresAt)<=Date.now())configureDiagnostics({enabled:false});for(const [controller,route] of managementRequests)if(route==='/api/device/ai')controller.abort();message=reason;vaultPanel=false;vault?.lock();tabs?.quarantine();emit();} function vaultSettings(){return {enabled:ready&&!!policy?.passwordManager?.enabled,allowSaving:!!policy?.passwordManager?.allowSaving,allowAutofill:!!policy?.passwordManager?.allowAutofill,idleLockSeconds:policy?.passwordManager?.idleLockSeconds||300};} function vaultStatus(){return {...(vault?.status()||{exists:fs.existsSync(path.join(app.getPath('userData'),'password-vault.json')),locked:true}),...vaultSettings()};} function requireVault(){ requireFeatureSection('passwords'); const settings=vaultSettings();if(!settings.enabled||Date.parse(policy.expiresAt)<=Date.now())throw Error('Password manager is disabled or browser access is paused'); requireEncryption(); if(!vault||vault.status().idleSeconds!==settings.idleLockSeconds){vault?.lock();vault=createVault({filePath:path.join(app.getPath('userData'),'password-vault.json'),idleSeconds:settings.idleLockSeconds,protect:bytes=>safeStorage.encryptString(bytes.toString('base64')),unprotect:bytes=>Buffer.from(safeStorage.decryptString(bytes),'base64')});} return vault; } function persistAudit(){requireEncryption();fs.writeFileSync(path.join(app.getPath('userData'),'audit-queue.bin'),safeStorage.encryptString(JSON.stringify(auditQueue)),{mode:0o600});} async function flushAudit(){ if(!device||flushing||!auditQueue.length)return;flushing=true; try{for(let n=0;n<100&&auditQueue.length;n++){await request(device.server,'/api/device/events',auditQueue[0],device.token);auditQueue.shift();persistAudit();}} catch{}finally{flushing=false;} } async function audit(kind,ruleId,host,fileFacts){ if(!device||!policy)return; // Operational correlation contains no rule text, hostname or forensic payload. try{diagnostics.event('browser.security',diagnosticContext({decision:kind.endsWith('.block')?'block':kind.endsWith('.denied')?'deny':'unknown'}));}catch{} if(auditQueue.length>=10000){quarantine('Security event queue is full. Contact your administrator.');return;} try{const file=fileFacts?require('../../packages/core/transfer-facts.cjs').transferFacts(fileFacts):undefined;auditQueue.push({eventId:require('node:crypto').randomUUID(),kind,...(ruleId?{ruleId}:{}),...(host?{host}:{}),...(file?{file}:{}),...diagnosticCorrelation(),policyVersion:policy.version});persistAudit();flushAudit();} catch{quarantine('Security events cannot be persisted. Contact your administrator.');} } function applyPolicy(envelope){return diagnosticSpan('browser.policy',{},()=>applyPolicyImpl(envelope));} async function applyPolicyImpl(envelope){ const next=assertPolicyAssignment(core.verifyPolicy(envelope,device.publicKey,device.minimumVersion||0),device.id); const nextBookmarks=(await import('../../packages/core/bookmarks.mjs')).verifyBookmarkSnapshot(envelope.bookmarks,device.publicKey,{deviceId:device.id,policyVersion:next.version,minimumRevision:device.bookmarkRevision||0}); const nextBookmarkJSON=JSON.stringify(nextBookmarks.items); if(nextBookmarks.revision===(device.bookmarkRevision||0)&&device.bookmarkDigest&&require('node:crypto').createHash('sha256').update(nextBookmarkJSON).digest('hex')!==device.bookmarkDigest||nextBookmarks.revision===personalBookmarks.revision&&bookmarkSnapshotJSON&&nextBookmarkJSON!==bookmarkSnapshotJSON)throw Error('Bookmarks changed without a new revision'); const acceptBookmarks=()=>{personalBookmarks={revision:nextBookmarks.revision,items:nextBookmarks.items};bookmarkSnapshotJSON=nextBookmarkJSON;device.bookmarkRevision=nextBookmarks.revision;device.bookmarkDigest=require('node:crypto').createHash('sha256').update(nextBookmarkJSON).digest('hex');saveDevice(device);}; if(process.env.SAFETYNET_POLICY_KEY_FILE&&fs.readFileSync(process.env.SAFETYNET_POLICY_KEY_FILE,'utf8').trim()!==device.publicKey.trim())throw Error('Policy signing key does not match deployment pin'); if(policy&&next.version===policy.version){ // Same versions must describe identical policy; reject equivocation. if(JSON.stringify(next)!==JSON.stringify(policy))throw Error('Policy changed without a new version');acceptBookmarks();if(Date.parse(policy.expiresAt)>Date.now())configureDiagnostics(policy.diagnostics);emit();return; } configureDiagnostics({enabled:false});quarantine('Applying company policy…'); const proxy=next.proxy; const settings={mode:proxy.mode,...(proxy.mode==='fixed_servers'?{proxyRules:proxy.rules,proxyBypassRules:proxy.bypass}:{}),...(proxy.mode==='pac_script'?{pacScript:proxy.pacUrl}:{})}; await browserSession.closeAllConnections();await browserSession.setProxy(settings); await managementSession.closeAllConnections();await managementSession.setProxy(settings); app.configureHostResolver(resolverOptions(next.enterprise?.dns)); await browserSession.clearHostResolverCache();recentRequests.clear(); await managementSession.clearHostResolverCache(); policy=next;device.minimumVersion=next.version;acceptBookmarks();configureDiagnostics(next.diagnostics); for(const tab of tabs.all()){await configureVisualPage(next.security.visualPhishing.enabled,tab);if(!next.browser.allowDevTools){tab.security.developerToolsGuard?.close();tab.view.webContents.closeDevTools();}} if((!next.agents.enabled||next.browser.settings.hiddenSections.includes('agents')||next.browser.settings.lockedSections.includes('agents'))&&agentService){agentService.revoke();await agentService.close();agentService=null;try{fs.unlinkSync(agentFile());}catch{}} await audit('policy.applied'); } async function sync(){ if(!device||syncing)return;syncing=true; try{ const envelope=await request(device.server,'/api/device/policy',null,device.token);await applyPolicy(envelope); await request(device.server,'/api/device/heartbeat',{policyVersion:policy.version},device.token); await flushAudit();if(auditQueue.length>=10000)throw Error('Security event queue is full'); // Mandatory local inspection must be attested before unlocking the browser; // first user gestures must not consume their scan budget waiting for ACL setup. if(policy.security.yara.enabled||policy.dlp.semantic.enabled){ if(!localInspector)throw Error('Required private content inspector is unavailable'); await diagnosticSpan('browser.parser',{operation:'scan'},()=>localInspector.waitReady()); } if(Date.parse(policy.expiresAt)<=Date.now())throw Error('Company policy expired'); if(clipboardFatal)throw Error('Managed renderer security failed; restart the browser'); const posture=await diagnosticSpan('browser.network',{operation:'scan'},()=>networkPosture.check(policy.enterprise?.vpn)); if(!posture.allowed)throw Error('Required corporate network is unavailable. Connect your company VPN.'); managementHealthy=true; const blocked=stickyBlock(); if(blocked&&blocked.policyVersion===policy.version&&(blockedTab()?.security.visualBlocked?policy.security.visualPhishing.enabled:policy.security.content.blockedPhrases.length)){message=blocked.message;ready=false;emit();layout();return;} for(const tab of tabs.all()){tab.security.visualBlocked=null;tab.security.contentBlocked=null;} const recovering=!ready;ready=true;tabs.resume(policy.version);message=`Managed · policy v${policy.version}`;emit();layout(); await ensureAgentAPI(); if(recovering){for(const tab of tabs.all()){const current=tab.view.webContents.getURL();if(/^https?:\/\//.test(current)&&core.destinationAllowed(current,policy))tab.view.webContents.reload();else if(current!=='about:blank'||tabs.all().length===1)await navigate({url:policy.browser.homeUrl,tabId:tab.id});}emit();} }catch(e){ // A deliberate page quarantine aborts in-flight management requests too. // Preserve that stronger, sticky verdict instead of replacing it with an // expected cancellation error or treating it as a management outage. const blocked=stickyBlock(); if(e.name==='AbortError'&&!ready&&blocked&&message===blocked.message){emit();return;} managementHealthy=false;quarantine(e.status===401?'Device revoked or credentials rejected. Contact your administrator.':`Browsing paused: ${e.message}`);if(e.status===401){configureDiagnostics({enabled:false});agentService?.revoke();await agentService?.close();agentService=null;try{fs.unlinkSync(agentFile());}catch{}await browserSession.closeAllConnections();await browserSession.clearStorageData();} } finally{syncing=false;} } async function completeEnrollment(server,result){ if(device)throw Error('This installation is already enrolled'); const candidate={server,id:result.id,token:result.token,publicKey:result.publicKey,minimumVersion:0}; candidate.channelSignatureKey=require('../../packages/core/policy-channel.cjs').verifyIdentity(result.managementChannel,candidate.publicKey); const channelPin=require('./management-channel.cjs').deploymentPin();if(channelPin&&candidate.channelSignatureKey.trim()!==channelPin)throw Error('Protected management signing pin mismatch'); // Optional deployment-time key pin strengthens first enrollment beyond TLS trust. if(process.env.SAFETYNET_POLICY_KEY_FILE && fs.readFileSync(process.env.SAFETYNET_POLICY_KEY_FILE,'utf8').trim()!==candidate.publicKey.trim())throw Error('Management signing key does not match deployment pin'); const initialPolicy=await require('./management-channel.cjs').protectedPolicyRequest({device:candidate,request,save:()=>{}}); assertPolicyAssignment(core.verifyPolicy(initialPolicy,candidate.publicKey),candidate.id); saveDevice(candidate);device=candidate;await sync();return state(); } function navigate(...args){return diagnosticSpan('browser.navigation',{},()=>navigateImpl(...args));} async function navigateImpl(input,captured,options={}){ const address=typeof input==='string'?input:input?.url; if(typeof address!=='string'||address.length>4096)throw Error('Invalid address'); const tab=tabs.get(captured?.sessionId||input?.tabId||activeTab().id),contents=tab.view.webContents; if(captured&&(!currentTabContext(captured.token)||captured.webContents!==contents))throw Error('Agent context changed'); const url=address.includes('://')?address:`https://${address}`; if(!policy||!['https:','http:'].includes(new URL(url).protocol)||!core.destinationAllowed(url,policy))throw Error('Destination is blocked by company policy'); const blocked=stickyBlock(),documentURL=value=>{const parsed=new URL(value);parsed.hash='';return parsed.href;}; if(blocked&&managementHealthy&&Date.parse(policy.expiresAt)>Date.now()&&blockedTab()===tab&&documentURL(url)!==documentURL(blocked.checkedURL)){ for(const record of tabs.all()){record.security.visualBlocked=null;record.security.contentBlocked=null;}ready=true;tabs.resume(policy.version);message='Checking destination…'; } if(!ready)throw Error('Browser is not managed and ready'); if(options.signal?.aborted)throw Error('Agent action cancelled'); const initial=policy;tab.startPage=false;if(tab===activeTab())startPage=false;layout(); const abort=()=>contents.stop();options.signal?.addEventListener('abort',abort,{once:true}); try{await contents.loadURL(url);}finally{options.signal?.removeEventListener('abort',abort);} if(options.signal?.aborted||!ready||policy!==initial||contents.isDestroyed()||tabs.fromContents(contents.id)!==tab)throw Error('Navigation context changed'); const refreshed=captured?agentContext(tab.id):null; if(captured?.token.foreground&&(!refreshed.foreground||captured.token.activation!==refreshed.token.activation))throw Error('Agent foreground changed during navigation'); emit();return refreshed||state(); } function agentContext(id){const tab=id?tabs.get(id):activeTab(),foreground=tab===activeTab();return {ready:ready&&featureAllowed(managedSettings(),'agents'),interactive:foreground&&!tab.startPage&&!overviewPanel&&!toolsOpen,policy,policyVersion:policy?.version,sessionId:tab.id,foreground,token:tabContext(tab.id,{foreground}),webContents:tab.view.webContents,destinationAllowed:core.destinationAllowed};} function managedAI(...args){return diagnosticSpan('browser.ai',{},()=>managedAIImpl(...args));} async function managedAIImpl(prompt){ requireFeatureSection('ai'); if(!ready||!policy?.ai.enabled)throw Error('AI access is disabled by company policy'); if(typeof prompt!=='string'||!prompt.trim()||prompt.length>100000)throw Error('Enter a prompt of up to 100,000 characters'); const initial=policy,verdict=core.inspectForAI(Buffer.from(prompt),initial); if(verdict.decision==='block'){await audit('dlp.block',verdict.ruleId);throw Error(`Blocked by company policy: ${verdict.ruleId}`);} if(initial.dlp.semantic.enabled||initial.security.yara.enabled){if(!localInspector)throw Error('Local inspector unavailable');const result=await diagnosticSpan('browser.inspect.local',{bytes:Buffer.byteLength(prompt)},()=>localInspector.inspect(Buffer.from(prompt),initial));if(result.decision!=='allow'&&!core.allowsConfidentialLocal(result,initial))throw Error('Local content policy blocked this prompt');} const external=await diagnosticSpan('browser.inspect.enterprise',{bytes:Buffer.byteLength(prompt)},()=>scanEnterprise(Buffer.from(prompt),initial,'ai'));if(external.decision!=='allow'){await audit('dlp.block',external.ruleId);throw Error('Company external scanner blocked this prompt');} if(!ready||policy.version!==initial.version)throw Error('Policy changed during inspection'); const result=await request(device.server,'/api/device/ai',{prompt},device.token); if(!ready||policy.version!==initial.version)throw Error('Policy changed during AI request');return result; } async function mutateManagedBookmarks(input){ if(bookmarkBusy||syncing)throw Error('Management update in progress. Try again.'); const initial=policy,identity=device; const payload=validateBookmarkMutation(input,{policy:effectiveBookmarkPolicy(),ready,settings:managedSettings(),destinationAllowed:core.destinationAllowed}); bookmarkBusy=true; try{ const result=await request(identity.server,'/api/device/bookmarks',payload,identity.token); if(policy!==initial||device!==identity)throw Error('Managed policy changed during bookmark update'); const deadline=Date.now()+12000;while(syncing){if(Date.now()>=deadline)throw Error('Bookmark update awaits management synchronization');await new Promise(resolve=>setTimeout(resolve,50));} await sync(); if(!ready||device!==identity||personalBookmarks.revision!==result.revision||policy.version!==result.version)throw Error('Bookmark update requires a fresh signed policy'); return state(); }finally{bookmarkBusy=false;} } async function ensureAgentAPI(){ if(!ready||!policy.agents.enabled||!featureAllowed(managedSettings(),'agents')||agentService||agentStarting)return;agentStarting=true; try{ const getContext=agentContext; const actions=createAgentAdapters({getContext,appControls:agentAppControls(),getSessions:()=>tabs.all().map(tab=>({id:tab.id,title:tab.title,foreground:tab===activeTab()})),contextCurrent:currentTabContext,navigate,ai:managedAI, inspect:async(text,context)=>{ const aiContext=context.action==='ai'; let verdict=aiContext?core.inspectForAI(Buffer.from(text),context.policy):core.inspect(Buffer.from(text),context.policy);if(verdict.decision==='block')return verdict; if(context.policy.dlp.semantic.enabled||context.policy.security.yara.enabled){ if(!localInspector)return {decision:'block'};const result=await diagnosticSpan('browser.inspect.local',{bytes:Buffer.byteLength(text)},()=>localInspector.inspect(Buffer.from(text),context.policy)); verdict=aiContext&&core.allowsConfidentialLocal(result,context.policy)?{...result,decision:'allow',classification:'local-only'}:result; }if(verdict.decision!=='allow')return verdict;const external=await diagnosticSpan('browser.inspect.enterprise',{bytes:Buffer.byteLength(text)},()=>scanEnterprise(Buffer.from(text),context.policy,aiContext?'ai':'upload'));return external.decision==='allow'?verdict:external; },audit:event=>audit('agent.denied',`${event.action}:${event.outcome}`)}); for(const action of ['click','type']){const original=actions[action];actions[action]=async(...args)=>{const tab=tabs.get(args[0]?.sessionId||activeTab().id),context=tabContext(tab.id);const checked=await checkVisual(tab);if(!currentTabContext(context)||checked.decision!=='allow')throw Error('Visual login protection has not approved this page');return original(...args);};} for(const action of Object.keys(actions)){const original=actions[action];actions[action]=(...args)=>diagnosticSpan('browser.agent',{operation:action},()=>original(...args));} agentService=await startAgentControl({actions, onError:error=>{if(!app.isPackaged&&process.env.SAFETYNET_SMOKE_ARTIFACTS)fs.appendFileSync(path.join(process.env.SAFETYNET_SMOKE_ARTIFACTS,'smoke-entry.txt'),`Agent action diagnostic: ${error.reason}\n`);}, authorize:({action,args,scope})=>{ if(!ready||!policy?.agents.enabled||Date.parse(policy.expiresAt)<=Date.now()||!policy.agents.scopes.includes(action))return {allowed:false}; if(action==='sessions'||Object.hasOwn(agentAppControls(),action))return {allowed:true,role:'agent',scopes:[scope]}; let tab;try{tab=args.sessionId?tabs.get(args.sessionId):activeTab();}catch{return {allowed:false};} const url=action==='navigate'?args.url:tab.view.webContents.getURL(); if(!core.destinationAllowed(url,policy))return {allowed:false}; if(policy.agents.allowedHosts.length&&!policy.agents.allowedHosts.some(pattern=>core.hostMatches(new URL(url).hostname,pattern)))return {allowed:false}; return {allowed:true,role:'agent',scopes:[scope]}; },audit:event=>audit(event.outcome==='denied'?'agent.denied':'agent.action',`${event.action}:${event.outcome}`), persistCredential:async connection=>{const protectedConnection=await require('./dpapi.cjs').capabilityCrypto('protect',JSON.stringify(connection));fs.writeFileSync(agentFile(),JSON.stringify({format:'windows-dpapi-v1',protectedConnection}),{mode:0o600});} });emit(); }finally{agentStarting=false;} } function dnsGate(...args){return diagnosticSpan('browser.dns',{},()=>dnsGateImpl(...args));} async function dnsGateImpl(url,currentPolicy){ const settings=currentPolicy.security.dns; if(!settings.enabled||!settings.blockedIPs.length)return; const host=new URL(url).hostname; if(securityCore.dnsAddressBlocked(host.replace(/^\[|\]$/g,''),currentPolicy))throw Error('dns-ip-blocklist'); let timer;const resolved=await Promise.race([browserSession.resolveHost(host,{cacheUsage:'allowed'}),new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error('dns-resolution-timeout')),5000);})]).finally(()=>clearTimeout(timer)); const addresses=resolved.endpoints?.map(v=>v.address)||[]; if(!addresses.length||addresses.some(ip=>securityCore.dnsAddressBlocked(ip,currentPolicy)))throw Error('dns-ip-blocklist'); } function hooks(){ browserSession.setPermissionRequestHandler((_wc,_permission,callback)=>callback(false)); browserSession.setPermissionCheckHandler(()=>false); const download=createDownloadGate({getContext:id=>{const tab=tabs.fromContents(id);return tab?{ready,policy,webContentsId:id,token:tabContext(tab.id,{foreground:false})}:null;},isCurrent:context=>!!context&¤tTabContext(context.token)&&policy===context.policy, fetchResponse:(url,options)=>diagnosticSpan('browser.request',{operation:'download'},()=>browserSession.fetch(url,options)),scan:(bytes,p)=>diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanDownloadEnterprise(bytes,p,localInspector)), choosePath:async name=>{const chosen=await dialog.showSaveDialog(win,{title:'Save scanned download (choose a new file)',defaultPath:path.join(app.getPath('downloads'),name)});return chosen.canceled?null:chosen.filePath;}, writeFile:async(file,bytes,current)=>{ if(!current()||!path.isAbsolute(file)||file.includes('\\\\?\\')||file.slice(2).includes(':'))throw Error('Save path rejected'); const handle=await fs.promises.open(file,'wx',0o600);let release=false; try{if(!current())throw Error('Policy changed');await handle.writeFile(bytes);await handle.sync();if(!current())throw Error('Policy changed');release=true;} finally{await handle.close();if(!release)await fs.promises.unlink(file).catch(()=>{});} },audit,status:text=>{message=text;emit();}}); browserSession.on('will-download',(event,item,contents)=>{ const url=item.getURL(),name=item.getFilename(),tracked=recentRequests.get(`${contents?.id}:${url}`);event.preventDefault(); if(!/^https?:\/\//.test(url)){audit('download.block','unsupported-download');return;} diagnosticSpan('browser.download',{},()=>download({url,name,method:tracked?.method,webContentsId:contents?.id})).catch(()=>audit('download.block','download-inspection-failed')); }); browserSession.webRequest.onBeforeRequest({urls:['']},(details,callback)=>{ // A delegated native frontend may load its bundled Chromium UI only. Its HTTP // requests receive no exemption, and neither pages nor trusted chrome qualify. if(developerAssetRequestAllowed(details,{ready,policy,tabs:tabs.all()})){callback({cancel:false});return;} let called=false;const finish=cancel=>{if(!called){called=true;callback({cancel});}}; const initialPolicy=policy,source=details.webContentsId>0?tabs.fromContents(details.webContentsId):null,sourceContext=source?tabContext(source.id,{foreground:false}):null; const sourceCurrent=()=>!sourceContext||currentTabContext(sourceContext);const timer=setTimeout(()=>finish(true),40000); // Async gates complete before Chromium may transmit the request. diagnosticSpan('browser.request',{requestType:details.resourceType,...(source?{sessionId:source.id}:{})},async()=>{try{ if(!ready||!policy||Date.parse(policy.expiresAt)<=Date.now()||(details.webContentsId>0&&(!source||!source.guarded))){finish(true);return;} if(!core.destinationAllowed(details.url,policy)){audit('navigation.block',null,new URL(details.url).hostname);finish(true);return;} recentRequests.set(`${details.webContentsId}:${details.url}`,{method:details.method,time:Date.now()});if(recentRequests.size>2000)for(const [url,value]of recentRequests)if(value.time2000)recentRequests.delete(url); try{await dnsGate(details.url,initialPolicy);}catch(error){audit('dns.block',error.message,new URL(details.url).hostname);finish(true);return;} if(!ready||policy!==initialPolicy||!sourceCurrent()){finish(true);return;} if(details.resourceType==='webSocket'&&!policy.browser.allowWebSockets){audit('dlp.block','websocket-disabled',new URL(details.url).hostname);finish(true);return;} const address=new URL(details.url),query=address.pathname+address.search; const queryVerdict=core.inspect(Buffer.from(query),policy); if(queryVerdict.decision==='block'){audit('dlp.block',queryVerdict.ruleId,new URL(details.url).hostname);finish(true);return;} const uploads=details.uploadData||[]; if(policy.security.visualPhishing.enabled&&(uploads.length||(address.search&&details.resourceType==='xhr')||details.resourceType==='webSocket')){ const checked=source?await checkVisual(source):{decision:'unknown'};if(checked.decision!=='allow'||!ready||policy!==initialPolicy||!sourceCurrent()){finish(true);return;} } // Electron file/data-pipe interception cannot bind scanning to the bytes later sent. // Block these paths rather than scanning a mutable file and allowing a race. if(uploads.some(part=>!part.bytes || part.file || part.blobUUID)){audit('dlp.block','uninspectable-upload',new URL(details.url).hostname);finish(true);return;} if(!['GET','HEAD','OPTIONS'].includes(details.method)&&!uploads.length){audit('dlp.block','missing-request-body',new URL(details.url).hostname);finish(true);return;} if(uploads.length){ if(uploads.reduce((n,p)=>n+p.bytes.length,0)>policy.dlp.maxBytes){audit('dlp.block','size-limit',new URL(details.url).hostname);finish(true);return;} const requestBytes=Buffer.concat(uploads.map(p=>Buffer.from(p.bytes))); const transfer=require('../../packages/core/file-transfer.cjs').inspectFileTransfer(requestBytes,policy,'upload'); const fileFacts={bytes:requestBytes.length,detectedType:transfer.detectedType,sha256:transfer.sha256}; const releaseUpload=async()=>{await audit('upload.authorized',null,new URL(details.url).hostname,fileFacts);finish(!ready||policy!==initialPolicy||!sourceCurrent());}; if(transfer.decision!=='allow'){await audit('upload.block',transfer.ruleId,new URL(details.url).hostname,fileFacts);requestBytes.fill(0);finish(true);return;} const verdict=core.inspect(requestBytes,policy);requestBytes.fill(0); if(verdict.decision==='block'&&verdict.ruleId!=='uninspectable'){audit('dlp.block',verdict.ruleId,new URL(details.url).hostname);finish(true);return;} if(verdict.ruleId==='uninspectable'||policy.dlp.semantic.enabled||policy.security.yara.enabled||icapRequired(policy,'upload')){ const needsLocal=verdict.ruleId==='uninspectable'||policy.dlp.semantic.enabled||policy.security.yara.enabled; if(needsLocal&&!localInspector){audit('dlp.block','inspector-unavailable',new URL(details.url).hostname);finish(true);return;} const snapshot=Buffer.concat(uploads.map(p=>Buffer.from(p.bytes))),currentPolicy=policy; try{let result=needsLocal?await diagnosticSpan('browser.inspect.local',{bytes:snapshot.length},()=>localInspector.inspect(snapshot,currentPolicy)):{decision:'allow'};if(result.decision==='allow')result=await diagnosticSpan('browser.inspect.enterprise',{bytes:snapshot.length},()=>scanEnterprise(snapshot,currentPolicy,'upload')); const cancel=!ready||policy.version!==currentPolicy.version||!sourceCurrent()||result.decision!=='allow'; if(cancel){audit('dlp.block',result.ruleId||'policy-changed',new URL(details.url).hostname);finish(true);}else await releaseUpload(); }catch{audit('dlp.block','inspection-incomplete',new URL(details.url).hostname);finish(true);}finally{snapshot.fill(0);}return; }else{await releaseUpload();return;} } finish(false); }catch{finish(true);}finally{clearTimeout(timer);}}).catch(()=>finish(true)); }); // Session request and permission gates are installed exactly once. } function installSiteHooks(tab){ const contents=tab.view.webContents,security=tab.security; const developerTools=createDeveloperToolsGuard({contents,getContext:()=>({ready,foreground:tab===activeTab()&&!tab.startPage&&!overviewPanel&&!toolsOpen,policy,url:contents.getURL(),token:tabContext(tab.id)}),isCurrent:context=>currentTabContext(context.token),audit:kind=>audit(kind),createFrontend:()=>{const frontend=new BrowserWindow({show:false,width:1000,height:750,title:'S Browser · Privileged developer tools',webPreferences:{session:browserSession,nodeIntegration:false,contextIsolation:true,sandbox:true,devTools:false}});security.developerFrontend=frontend.webContents;frontend.webContents.setWindowOpenHandler(()=>({action:'deny'}));return frontend;}}); security.developerToolsGuard=developerTools; contents.setWindowOpenHandler(({url})=>{createNativeTab({url}).catch(e=>{message=e.message;emit();});return {action:'deny'};}); contents.on('will-navigate',(event,url)=>{if(!ready||!core.destinationAllowed(url,policy))event.preventDefault();}); contents.on('will-attach-webview',event=>event.preventDefault()); contents.on('before-input-event',(event,input)=>{if(developerTools.handleInput(event,input))return;const approval=security.visualApproval;if(policy?.security.visualPhishing.enabled&&(!approval||approval.generation!==tab.navigationId||approval.version!==policy.version||approval.url!==contents.getURL())){event.preventDefault();checkVisual(tab).catch(()=>{});}}); contents.on('did-start-navigation',(_event,_url,_inPlace,isMainFrame)=>{if(isMainFrame){tabs.navigation(tab.id);if(_url!=='about:blank')tab.startPage=false;}}); contents.on('did-navigate',()=>emit());contents.on('did-navigate-in-page',()=>emit()); contents.on('did-finish-load',()=>pageTheme(tab));contents.on('page-title-updated',()=>{tab.title=contents.getTitle().slice(0,200);if(tab===activeTab())pageTitle=tab.title;emit();}); installClipboardControls(contents); security.visualScanner=createVisualPhishing({getContext:()=>({ready,policy,webContents:contents,url:contents.getURL(),navigationId:tab.navigationId}), describe:url=>visualDeadline(contents.executeJavaScriptInIsolatedWorld(999,[{code:describeScript(url)}],false)), capture:async(scope,description)=>{let rect;if(scope==='login-form'){const r=description.formRect;if(!r)throw Error('Login region unavailable');rect={x:Math.floor(r.x),y:Math.floor(r.y),width:Math.ceil(r.width),height:Math.ceil(r.height)};}const bytes=await visualDeadline(capturePNG(contents,rect));if(bytes.length<24)throw Error('Capture dimensions unavailable');return {bytes,width:bytes.readUInt32BE(16),height:bytes.readUInt32BE(20)};}, fingerprint:capture=>{try{return fingerprintPNG(capture.bytes);}finally{capture.bytes.fill(0);}},compare:hammingDistance, audit:event=>audit(event.kind,event.ruleId,event.host),block:result=>{security.visualBlocked={...result,message:result.reason==='credential-brand-impersonation'?'Blocked: this login resembles a protected brand on an unapproved origin.':'Blocked: this login could not be inspected safely.'};security.visualApproval=null;quarantine(security.visualBlocked.message);} }); } function createNativeTab(input={}){return diagnosticSpan('browser.tab',{operation:'create'},()=>createNativeTabImpl(input));} async function createNativeTabImpl(input={}){ if(!input||typeof input!=='object'||Object.keys(input).some(key=>!['group','url'].includes(key)))throw Error('Invalid native tab request'); if(input.group)requireManagedSection('library'); const tab=tabs.create({group:input.group||''});installSiteHooks(tab); try{ await tab.view.webContents.loadURL('about:blank'); await diagnosticSpan('browser.clipboard',{operation:'create',sessionId:tab.id},()=>attachPageClipboardGuard(tab.view.webContents,()=>{clipboardFatal=true;quarantine('Managed clipboard protection failed; restart the browser');audit('clipboard.block','clipboard-primitive-unavailable');app.quit();})); tab.guarded=true;selectTab(tab.id); if(input.url)await navigate({url:input.url,tabId:tab.id});emit();return state(); }catch(error){if(tabs.all().includes(tab)){tabs.remove(tab.id);if(tabs.active()?.guarded)selectTab(tabs.active().id);}throw error;} } function updateManagedAppearance(input){requireManagedSection('appearance');appearancePreferences={...appearancePreferences,...validateAppearance(input)};persistAppearance();emit();return managedSettings();} async function closeNativeTab(id){tabs.remove(id);if(!tabs.all().length)await createNativeTab();else selectTab(tabs.active().id);emit();return state();} function groupNativeTab(input){requireManagedSection('library');if(!input||typeof input.group!=='string'||input.group.length>40)throw Error('Invalid native tab group');tabs.get(input.id).group=input.group;emit();return state();} function agentAppControls(){ const checked=(operation)=>async(args,{signal,policyVersion}={})=>{ if(signal?.aborted||!ready||policy?.version!==policyVersion||!featureAllowed(managedSettings(),'agents'))throw Error('Managed application changed'); return operation(args); }; return { 'application:state':checked(()=>({ready,policyVersion:policy.version,activeTabId:activeTab()?.id,tabCount:tabs.all().length})), 'settings:get':checked(()=>managedSettings()), 'settings:update':checked(args=>updateManagedAppearance(args.appearance)), 'bookmarks:list':checked(()=>({...bookmarkState({policy:effectiveBookmarkPolicy(),ready,settings:managedSettings()}),policyVersion:policy.version})), 'bookmarks:mutate':checked(async args=>{await mutateManagedBookmarks(args);return {completed:true};}), 'tabs:create':checked(async args=>{const result=await createNativeTab(args);return {completed:true,sessionId:result.activeTabId};}), 'tabs:activate':checked(args=>{selectTab(args.sessionId);emit();return {completed:true,sessionId:args.sessionId};}), 'tabs:close':checked(async args=>{await closeNativeTab(args.sessionId);return {completed:true};}), 'tabs:group':checked(args=>{groupNativeTab({id:args.sessionId,group:args.group});return {completed:true,sessionId:args.sessionId};}) }; } function ipc(channel,fn){ipcMain.handle(channel,async(event,...args)=>{ if(event.sender!==win.webContents || event.senderFrame!==win.webContents.mainFrame || event.senderFrame?.url!==uiUrl)throw Error('Untrusted IPC sender'); try{return {ok:true,data:await diagnosticSpan('browser.ipc',{operation:channel.slice('managed:'.length)},()=>fn(...args))};}catch(e){return {ok:false,error:e.message};} });} app.whenReady().then(async()=>{ if(!ownsBrowserInstance)return; if(cliRequest){try{await require('./agent-client.cjs').runClient(cliConnection,cliRequest);}catch{console.error('Local agent action failed. Check policy and encrypted pairing file.');app.exit(1);}return;} core=await import('../../packages/core/policy.mjs'); clipboardCore=await import('../../packages/core/clipboard.mjs'); ipcMain.on('managed:clipboard-sandbox-failure',event=>{if(tabs?.fromContents(event.sender.id)||event.sender===win?.webContents){clipboardFatal=true;quarantine('Managed renderer security failed. Restart this browser.');}}); powerMonitor.on('lock-screen',lockVault);powerMonitor.on('suspend',lockVault); securityCore=await import('../../packages/core/security.mjs'); localInspector=startInspector(app.isPackaged?process.resourcesPath:path.join(__dirname,'../..'),app.isPackaged); browserSession=session.fromPartition('persist:safetynet-managed',{cache:false}); // Management has its own cookie jar and no website request gate. It uses the // same company proxy and Chromium DNS configuration as managed website traffic. managementSession=session.fromPartition('persist:safetynet-management',{cache:false}); browserSession.registerPreloadScript({type:'frame',id:'managed-clipboard-deny',filePath:path.join(__dirname,'clipboard-deny.cjs')}); session.defaultSession.registerPreloadScript({type:'frame',id:'shell-clipboard-deny',filePath:path.join(__dirname,'clipboard-deny.cjs')}); Menu.setApplicationMenu(null); win=new BrowserWindow({width:1320,height:900,minWidth:800,minHeight:600,frame:false,roundedCorners:true,hasShadow:true,backgroundMaterial:'acrylic',backgroundColor:'#eef1f5',autoHideMenuBar:true,webPreferences:{preload:path.join(__dirname,'preload.cjs'),nodeIntegration:false,contextIsolation:true,sandbox:true,devTools:false}}); win.removeMenu(); tabs=createTabRegistry({createView:()=>new WebContentsView({webPreferences:{session:browserSession,preload:path.join(__dirname,'site-preload.cjs'),nodeIntegration:false,nodeIntegrationInSubFrames:true,contextIsolation:true,sandbox:true,webSecurity:true,allowRunningInsecureContent:false}}),attach:v=>win.contentView.addChildView(v),detach:v=>win.contentView.removeChildView(v),dispose:tab=>disposePageClipboardGuard(tab.view.webContents)}); const executeClipboard=(mode,args)=>args.webContents.executeJavaScriptInIsolatedWorld(999,[{code:clipboardScript(mode,args)}],false); clipboardGuard=createClipboardGuard({ getContext:contents=>{const target=contents||view?.webContents,tab=target&&tabs.fromContents(target.id),site=!!tab,url=target?.getURL()||'',token=site?tabContext(tab.id):null;return {ready:ready&&target?.isFocused()&&(site?tab===activeTab()&&!tab.startPage&&!overviewPanel&&!toolsOpen&&core.destinationAllowed(url,policy):target===win.webContents&&url===uiUrl),policy,webContents:target,url,navigationId:site?tab.navigationId:0,tabId:tab?.id,activation:token?.activation};}, stage:async args=>{const tab=tabs.fromContents(args.webContents.id);if(tab&&args.operation==='paste'&&policy?.security.visualPhishing.enabled&&(await checkVisual(tab)).decision!=='allow')throw Error('Login protection holds paste');return executeClipboard('stage',args);}, validate:args=>executeClipboard('validate',args),commit:args=>executeClipboard('commit',args), discard:args=>executeClipboard('discard',{...args,url:'',operation:'copy',limit:0}), readClipboard:async limit=>{const items=await clipboard.read();if(items.length!==1)throw Error('Clipboard format unsupported');const item=items[0];if(!item.types.includes('text/plain')||item.types.some(type=>type!=='text/plain'&&!/^electron application\/osclipboard;format="(?:CF_TEXT|CF_UNICODETEXT|CF_LOCALE)"$/.test(type)))throw Error('Rich clipboard denied');const blob=await item.getType('text/plain');if(blob.size>limit)throw Error('Clipboard size limit');return blob.text();}, writeClipboard:text=>clipboard.writeText(text), inspect:async(bytes,currentPolicy,operation)=>{let verdict=clipboardCore.inspectClipboard(bytes,currentPolicy,operation);if(verdict.decision!=='allow')return verdict;if(currentPolicy.dlp.semantic.enabled||currentPolicy.security.yara.enabled){if(!localInspector)return {decision:'block',ruleId:'clipboard-inspector-unavailable'};verdict=await diagnosticSpan('browser.inspect.local',{bytes:bytes.length},()=>localInspector.inspect(bytes,currentPolicy));}if(verdict.decision!=='allow')return verdict;const external=await diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanEnterprise(bytes,currentPolicy,'clipboard'));return external.decision==='allow'?verdict:external;}, audit:async(kind,ruleId)=>{await audit(kind,ruleId);if(kind==='clipboard.block'&&ready){message=`Clipboard blocked by company policy (${ruleId}).`;emit();}} }); installClipboardControls(win.webContents);hooks(); ipcMain.on('managed:visual-policy',event=>{event.returnValue={enabled:!!tabs.fromContents(event.sender.id)&&!!policy?.security.visualPhishing.enabled};}); ipcMain.handle('managed:visual-check',async(event,input)=>{ const tab=tabs.fromContents(event.sender.id);if(!tab||event.senderFrame!==event.sender.mainFrame||!input||typeof input.token!=='string'||input.token.length>80||!Number.isSafeInteger(input.revision)||input.revision<0||input.url!==event.sender.getURL())return {decision:'unknown'}; const security=tab.security;if(input.token!==security.visualToken||input.revision!==security.visualRevision){tabs.navigation(tab.id);security.visualToken=input.token;security.visualRevision=input.revision;} try{return await checkVisual(tab);}catch{return {decision:'unknown'};} }); await createNativeTab(); win.webContents.setWindowOpenHandler(()=>({action:'deny'}));win.webContents.on('will-navigate',event=>event.preventDefault()); ipc('managed:state',()=>({...state(),pageTitle,pageColor,windowMaximized:win.isMaximized()})); ipc('managed:window-control',action=>{if(action==='minimize')win.minimize();else if(action==='maximize'){win.isMaximized()?win.unmaximize():win.maximize();}else if(action==='close')win.close();else throw Error('Unknown window action');}); ipc('managed:authorize-setting',section=>{requireManagedSection(section);return {authorized:true,policyVersion:policy.version};}); ipc('managed:bookmarks-mutate',mutateManagedBookmarks); ipc('managed:settings-update',updateManagedAppearance); ipc('managed:chrome-layout',async input=>{ if(!input||![64,104].includes(input.height)||['sidebarOpen','startPageOpen','overviewOpen'].some(key=>typeof input[key]!=='boolean'))throw Error('Unsupported browser layout'); if(managedSettings().available&&!sectionAllowed(managedSettings(),'appearance')&&input.height!==(managedSettings().appearance.tabMode==='compact'?64:104))throw Error('Tab layout is controlled by your administrator'); if(input.toolsOpen&&!managedSettings().visible)throw Error('Settings access is disabled by your administrator'); if(input.sidebarOpen&&!libraryVisible(managedSettings()))throw Error('Library access is controlled by your administrator'); if(input.toolsOpen!==undefined&&typeof input.toolsOpen!=='boolean')throw Error('Unsupported tools layout');chromeHeight=input.height;leftSidebar=input.sidebarOpen;startPage=activeTab()?.startPage??true;overviewPanel=input.overviewOpen;toolsOpen=!!input.toolsOpen;layout(); if(policy?.security.visualPhishing.enabled)await configureVisualPage(true,activeTab());return {height:chromeHeight}; }); ipc('managed:share-url',async()=>{ if(!ready)throw Error('Browser access paused');const initial=policy,context=tabContext(),contents=context.webContents,pageURL=contents.getURL(),url=new URL(pageURL); if(!['https:','http:'].includes(url.protocol)||url.username||url.password)throw Error('Cannot share this page');url.search='';url.hash='';const text=url.toString(),bytes=Buffer.from(text); let verdict=clipboardCore.inspectClipboard(bytes,initial,'copy'); if(verdict.decision==='allow'&&(initial.dlp.semantic.enabled||initial.security.yara.enabled))verdict=localInspector?await diagnosticSpan('browser.inspect.local',{bytes:bytes.length},()=>localInspector.inspect(bytes,initial)):{decision:'block',ruleId:'clipboard-inspector-unavailable'}; if(verdict.decision==='allow')verdict=await diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanEnterprise(bytes,initial,'clipboard')); if(verdict.decision!=='allow'){await audit('clipboard.block',verdict.ruleId);throw Error('Company policy blocks sharing this link');} await audit('clipboard.copy','authorized-link'); if(!ready||policy!==initial||Date.parse(initial.expiresAt)<=Date.now()||!currentTabContext(context))throw Error('Browser state changed during link inspection'); await clipboard.writeText(text);return {copied:true}; }); ipc('managed:enroll',async input=>{ if(device)throw Error('Already enrolled');requireEncryption(); if(!input||typeof input.server!=='string'||typeof input.code!=='string'||input.code.length>200)throw Error('Invalid enrollment'); const server=managementUrl(input.server),result=await request(server,'/api/device/enroll',{code:input.code,name:os.hostname(),platform:process.platform}); return completeEnrollment(server,result); }); ipc('managed:sso',async input=>{ if(device)throw Error('Already enrolled');requireEncryption();const server=managementUrl(input.server); const ticket=await request(server,'/api/device/sso/start',{}); const loginUrl=new URL(ticket.url);if(loginUrl.origin!==server || loginUrl.pathname!=='/auth/login')throw Error('Invalid login URL'); await shell.openExternal(loginUrl.toString()); message='Complete enterprise sign-in in your system browser…';emit(); for(let attempt=0;attempt<100;attempt++){ await new Promise(resolve=>setTimeout(resolve,3000)); const result=await request(server,'/api/device/sso/poll',{id:ticket.id,secret:ticket.secret,name:os.hostname(),platform:process.platform}); if(!result.pending)return completeEnrollment(server,result); } throw Error('Enterprise sign-in expired'); }); ipc('managed:tab-create',createNativeTab); ipc('managed:tab-activate',id=>{selectTab(id);emit();return state();}); ipc('managed:tab-close',closeNativeTab); ipc('managed:tab-group',groupNativeTab); ipc('managed:navigate',navigate); ipc('managed:back',()=>{if(ready&&view.webContents.navigationHistory.canGoBack())view.webContents.navigationHistory.goBack();}); ipc('managed:forward',()=>{if(ready&&view.webContents.navigationHistory.canGoForward())view.webContents.navigationHistory.goForward();}); ipc('managed:reload',()=>{if(ready)view.webContents.reload();}); ipc('managed:ai-panel',open=>{if(open)requireFeatureSection('ai');aiPanel=!!open;if(aiPanel){vaultPanel=false;lockVault();}layout();}); ipc('managed:vault-panel',open=>{if(open)requireVault();vaultPanel=!!open;if(vaultPanel)aiPanel=false;else lockVault();layout();}); ipc('managed:vault-status',()=>{requireFeatureSection('passwords');return vaultStatus();}); ipc('managed:vault-create',async master=>{requireVault().create(master);await audit('vault.created');if(!ready){lockVault();throw Error('Browser access paused');}emit();return vaultStatus();}); ipc('managed:vault-unlock',async master=>{requireVault().unlock(master);await audit('vault.unlocked');if(!ready){lockVault();throw Error('Browser access paused');}emit();return vaultStatus();}); ipc('managed:vault-lock',()=>{lockVault();return vaultStatus();}); ipc('managed:vault-list',()=>requireVault().list()); ipc('managed:vault-save',async entry=>{if(!vaultSettings().allowSaving)throw Error('Saving passwords is disabled by company policy');const result=requireVault().upsert(entry);await audit('vault.saved');emit();return result;}); ipc('managed:vault-delete',async id=>{if(!vaultSettings().allowSaving)throw Error('Password changes are disabled by company policy');const result=requireVault().delete(id);await audit('vault.deleted');return result;}); ipc('managed:vault-generate',()=>{const current=requireVault();if(!vaultSettings().allowSaving||current.status().locked)throw Error('Unlock the vault before generating a password');return current.generatePassword();}); ipc('managed:vault-fill',async id=>{ const context=tabContext(),tab=tabs.get(context.tabId),contents=context.webContents; if(startPage||overviewPanel||toolsOpen)throw Error('Show the saved login website before filling credentials'); if((await checkVisual(tab)).decision!=='allow'||!currentTabContext(context))throw Error('Visual login protection has not approved this page'); if(startPage||overviewPanel||toolsOpen)throw Error('The login website is no longer visible'); const current=requireVault(),initial=policy,url=context.url; if(!initial.passwordManager.allowAutofill||!core.destinationAllowed(url,initial)||new URL(url).protocol!=='https:')throw Error('Password filling requires an approved HTTPS destination'); let credential; try{ const origin=new URL(url).origin;credential={...current.credential(id,origin),origin}; await audit('vault.fill-requested'); if(!currentTabContext(context)||startPage||overviewPanel||toolsOpen||policy!==initial||Date.parse(initial.expiresAt)<=Date.now()||current.status().locked)throw Error('Browser state changed'); contents.focus();if(!currentTabContext(context))throw Error('Browser state changed'); const result=await diagnosticSpan('browser.password.fill',{sessionId:context.tabId},()=>contents.executeJavaScriptInIsolatedWorld(999,[{code:fillScript(url,credential,Date.parse(initial.expiresAt))}],false)); if(!currentTabContext(context)||startPage||overviewPanel||toolsOpen||policy!==initial||current.status().locked||result?.completed!==true)throw Error('Browser state changed'); await audit('vault.filled');return {completed:true}; }catch{throw Error('Cannot fill this login. Unlock the vault and use its exact HTTPS origin with a single visible same-origin POST login form.');} finally{if(credential){credential.username='';credential.password='';}} }); ipc('managed:ai',managedAI); ipc('managed:agent-connection',()=>{ requireFeatureSection('agents'); if(!ready||!policy.agents.enabled||!agentService)throw Error('Agent access is disabled by company policy');return {address:agentService.address,credentialPath:agentFile()}; }); ipcMain.on('managed:page-content',async(event,input)=>{ const tab=tabs.fromContents(event.sender.id);if(!tab||event.senderFrame!==event.sender.mainFrame||!ready||typeof input?.text!=='string'||input.text.length>250000)return; const initial=policy,context=tabContext(tab.id,{foreground:false});if(!initial.security.content.blockedPhrases.length)return; const result=core.inspect(Buffer.from(input.text),initial);if(result.decision==='block'&&result.ruleId?.startsWith('content-')&¤tTabContext(context)&&policy===initial){await audit('content.block',result.ruleId);if(!currentTabContext(context))return;tab.security.contentBlocked={checkedURL:context.url,policyVersion:initial.version,message:'This page contains content blocked by company policy.'};quarantine(tab.security.contentBlocked.message);} }); ipc('managed:inspect-file',async()=>{ requireFeatureSection('documentScan'); if(!ready||!policy)throw Error('Device must be managed before inspecting files'); if(policy.inspection?.documentAnalysisEnabled===false)throw Error('Document analysis is disabled for this browser by your administrator'); if(!localInspector)throw Error('Local content inspector is not installed'); const currentPolicy=policy; const selection=await dialog.showOpenDialog(win,{title:icapRequired(policy,'upload')?'Inspect content with company security services':'Inspect company content locally',properties:['openFile']});if(selection.canceled)return null; requireFeatureSection('documentScan'); if(!ready||policy!==currentPolicy)throw Error('Policy changed during file selection; inspect again'); const file=selection.filePaths[0],handle=await fs.promises.open(file,'r');let bytes; try{const info=await handle.stat();if(!info.isFile()||info.size>policy.dlp.maxBytes)throw Error('File exceeds company inspection limit'); // Read a bounded immutable snapshot, never release the subsequently mutable original. const chunks=[];let size=0;for await(const chunk of handle.createReadStream()){size+=chunk.length;if(size>policy.dlp.maxBytes)throw Error('File exceeds company inspection limit');chunks.push(chunk);}bytes=Buffer.concat(chunks); }finally{await handle.close();} if(!ready||policy!==currentPolicy){bytes.fill(0);throw Error('Policy changed during file selection; inspect again');} let result; try{result=await diagnosticSpan('browser.inspect.local',{bytes:bytes.length},()=>localInspector.inspect(bytes,currentPolicy));if(result.decision==='allow'&&icapRequired(currentPolicy,'upload')){const external=await diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanEnterprise(bytes,currentPolicy,'upload'));if(external.decision!=='allow')result=external;}}finally{bytes.fill(0);} if(!ready||currentPolicy.version!==policy.version)throw Error('Policy changed during inspection; inspect again'); if(result.decision==='block')await audit('dlp.block',result.ruleId); return {name:path.basename(file),...result}; }); win.on('close',()=>tabs.close()); win.on('resize',layout);win.on('maximize',emit);win.on('unmaximize',emit);win.on('blur',lockVault);win.on('minimize',lockVault);win.on('closed',()=>{vault?.lock();clearInterval(vaultTimer);win=null;clearInterval(syncTimer);localInspector?.stop();agentService?.close();}); loadAppearance(); await win.loadFile(path.join(__dirname,'index.html'));layout(); try{device=loadDevice();const queuePath=path.join(app.getPath('userData'),'audit-queue.bin');if(device&&fs.existsSync(queuePath))auditQueue=JSON.parse(safeStorage.decryptString(fs.readFileSync(queuePath)));if(device){await sync();await audit('browser.started');}}catch(e){quarantine(`Enrollment could not load: ${e.message}`);} const refreshMs=!app.isPackaged&&process.env.SAFETYNET_SMOKE_USERDATA===app.getPath('userData')?1000:15000; syncTimer=setInterval(()=>{if(policy&&Date.parse(policy.expiresAt)<=Date.now())quarantine('Company policy expired');sync();},refreshMs); networkTimer=setInterval(async()=>{ if(!policy?.enterprise?.vpn.required||networkChecking)return;networkChecking=true; const checkedPolicy=policy; try{const posture=await diagnosticSpan('browser.network',{operation:'scan'},()=>networkPosture.check(checkedPolicy.enterprise.vpn));if(policy===checkedPolicy&&!posture.allowed){managementHealthy=false;quarantine('Required corporate network is unavailable. Connect your company VPN.');await Promise.all([browserSession.closeAllConnections(),managementSession.closeAllConnections()]);}} finally{networkChecking=false;} },5000); let wasLocked=true;vaultTimer=setInterval(()=>{const locked=vault?.status().locked??true;if(locked!==wasLocked){if(locked)audit('vault.locked');wasLocked=locked;emit();}},1000); }).catch(error=>{console.error(error.stack);app.quit();}); app.on('window-all-closed',()=>app.quit()); app.on('before-quit',()=>{tabs?.close();}); app.on('will-quit',()=>{Promise.resolve(diagnostics.shutdown()).catch(()=>{});vault?.lock();clearInterval(vaultTimer);clearInterval(networkTimer);localInspector?.stop();agentService?.close();}); app.on('certificate-error',(event,_wc,_url,_error,_certificate,callback)=>{event.preventDefault();callback(false);}); app.on('login',(event,_wc,_details,authInfo,callback)=>{ event.preventDefault(); // Proxy credentials are supplied by deployment environment, never a page or policy bundle. if(authInfo.isProxy&&process.env.SAFETYNET_PROXY_USER&&process.env.SAFETYNET_PROXY_PASSWORD)callback(process.env.SAFETYNET_PROXY_USER,process.env.SAFETYNET_PROXY_PASSWORD); else callback(); });