Add policy-bound credential submission and native DLP audit proofs
This commit is contained in:
1 parent
b991ee9da6
commit
c8b6ff9cdd
45 files changed
+777
-98
No files matched your search
+68
-19
@@ -179,9 +179,9 @@ async function pageTheme(tab=activeTab()){
|
||||
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 lockVault(){const unlocked=vault&&!vault.status().locked;credentialBroker?.revoke();vault?.lock();if(unlocked)audit('vault.locked');emit();}
|
||||
function quarantine(reason){ready=false;fileUploadGate?.cancelAll();if(policy&&Date.parse(policy.expiresAt)<=Date.now()){configureDiagnostics({enabled:false});const expiredService=agentService;agentService=null;agentServiceMode=undefined;expiredService?.revoke();void expiredService?.close().catch(()=>{});}for(const [controller,route] of managementRequests)if(route==='/api/device/ai')controller.abort();enforceEvidencePolicy();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 vaultSettings(){return {enabled:ready&&!!policy?.passwordManager?.enabled,allowSaving:!!policy?.passwordManager?.allowSaving,allowAutofill:!!policy?.passwordManager?.allowAutofill,allowControlledSubmission:!!policy?.passwordManager?.controlledSubmission?.enabled,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');
|
||||
@@ -207,12 +207,12 @@ async function flushAudit(){
|
||||
try{for(let n=0;n<100&&auditQueue.length;n++){const record=auditQueue[0];await diagnostics.withCorrelation(record,()=>diagnosticSpan('browser.management',{operation:'audit.deliver'},()=>require('./evidence-delivery.cjs').deliverAuditRecord({record,send:(route,body)=>request(device.server,route,body,device.token),getContext:()=>({ready,policy,deviceId:device.id})})));auditQueue.shift();persistAudit();}}
|
||||
catch{}finally{flushing=false;}
|
||||
}
|
||||
async function audit(kind,ruleId,host,fileFacts,requestFacts,evidencePayload,agentFacts,interactionFacts){
|
||||
async function audit(kind,ruleId,host,fileFacts,requestFacts,evidencePayload,agentFacts,interactionFacts,origin){
|
||||
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':kind.endsWith('.authorized')||kind.endsWith('.released')?'allow':'unknown',reason:ruleId}));}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;const request=requestFacts?require('../../packages/core/request-transfer-facts.cjs').requestTransferFacts(requestFacts):undefined;const agent=agentFacts?require('../../packages/core/agent-audit.cjs').agentAuditFacts(agentFacts):undefined;const interaction=interactionFacts?require('../../packages/core/interaction-facts.cjs').InteractionFactsSchema.parse(interactionFacts):undefined;const evidenceType=evidencePayload?.action?'interaction':'download';auditQueue.push({eventId:require('node:crypto').randomUUID(),kind,...(ruleId?{ruleId}:{}),...(host?{host}:{}),...(file?{file}:{}),...(request?{request}:{}),...(agent?{agent}:{}),...(interaction?{interaction}:{}),...(evidencePayload&&require('./evidence-delivery.cjs').collectionAllowed({ready,policy,type:evidenceType})?{protectedEvidence:{deviceId:device.id,type:evidenceType,payload:require('./evidence-delivery.cjs').validatedEvidence(evidencePayload,evidenceType)}}:{}),...diagnosticCorrelation(),policyVersion:policy.version});persistAudit();const eventId=auditQueue.at(-1).eventId;flushAudit();return eventId;}
|
||||
try{const file=fileFacts?require('../../packages/core/transfer-facts.cjs').transferFacts(fileFacts):undefined;const request=requestFacts?require('../../packages/core/request-transfer-facts.cjs').requestTransferFacts(requestFacts):undefined;const agent=agentFacts?require('../../packages/core/agent-audit.cjs').agentAuditFacts(agentFacts):undefined;const interaction=interactionFacts?require('../../packages/core/interaction-facts.cjs').InteractionFactsSchema.parse(interactionFacts):undefined;const evidenceType=evidencePayload?.action?'interaction':'download';auditQueue.push({eventId:require('node:crypto').randomUUID(),kind,...(ruleId?{ruleId}:{}),...(host?{host}:{}),...(file?{file}:{}),...(request?{request}:{}),...(agent?{agent}:{}),...(interaction?{interaction}:{}),...(evidencePayload&&require('./evidence-delivery.cjs').collectionAllowed({ready,policy,type:evidenceType})?{protectedEvidence:{deviceId:device.id,type:evidenceType,payload:require('./evidence-delivery.cjs').validatedEvidence(evidencePayload,evidenceType)}}:{}),...diagnosticCorrelation(),policyVersion:Number.isSafeInteger(origin?.policyVersion)&&origin.policyVersion>0?origin.policyVersion:policy.version});persistAudit();const eventId=auditQueue.at(-1).eventId;flushAudit();return eventId;}
|
||||
catch{quarantine('Security events cannot be persisted. Contact your administrator.');}
|
||||
}
|
||||
function auditAgent(event){
|
||||
@@ -245,7 +245,7 @@ async function applyPolicyImpl(envelope){
|
||||
app.configureHostResolver(resolverOptions(next.enterprise?.dns));
|
||||
await browserSession.clearHostResolverCache();recentRequests.clear();
|
||||
await managementSession.clearHostResolverCache();
|
||||
policy=next;enforceEvidencePolicy();device.minimumVersion=next.version;acceptBookmarks();configureDiagnostics(next.diagnostics);
|
||||
credentialBroker?.revoke();policy=next;enforceEvidencePolicy();device.minimumVersion=next.version;acceptBookmarks();configureDiagnostics(next.diagnostics);
|
||||
await diagnosticSpan('browser.policy',{operation:'policy.page-configure'},async()=>{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();}}});
|
||||
await diagnosticSpan('browser.policy',{operation:'policy.agent-reconcile'},()=>reconcileAgentPolicy());
|
||||
await diagnosticSpan('browser.policy',{operation:'policy.audit'},()=>audit('policy.applied'));
|
||||
@@ -419,7 +419,7 @@ async function ensureAgentAPI(){
|
||||
Object.assign(actions,agentSessionControls());
|
||||
for(const action of ['click','type','select','check']){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,context={})=>agentActorStorage.run({...context,action},()=>diagnosticSpan('browser.agent',{operation:action},async()=>{
|
||||
if(context.principalId&&['navigate','back','forward','reload','history:go','vault:fill','click','type','select','check'].includes(action)){const tab=tabs.get(args.sessionId||activeTab().id);tab.security.agentAuthority={principalId:context.principalId,generation:context.generation,action};}
|
||||
if(context.principalId&&['navigate','back','forward','reload','history:go','vault:fill','vault:prepare-submit','click','type','select','check'].includes(action)){const tab=tabs.get(args.sessionId||activeTab().id);tab.security.agentAuthority={principalId:context.principalId,generation:context.generation,action};}
|
||||
return original(args,context);
|
||||
}));}
|
||||
agentServiceMode=policy.agents.identityMode;agentService=await startAgentControl({actions,principalMode:agentServiceMode,principals:agentServiceMode==='individual'?effectiveAgentPolicy().agents.grants:[],
|
||||
@@ -485,9 +485,12 @@ function hooks(){
|
||||
return diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanEnterprise(bytes,p,'upload'));
|
||||
})});
|
||||
for(const event of ['onCompleted','onErrorOccurred'])browserSession.webRequest[event]({urls:['<all_urls>']},details=>{
|
||||
credentialBroker?.complete(details.id);const record=credentialRequestCorrelations.get(details.id);credentialRequestCorrelations.delete(details.id);
|
||||
if(record?.released&&record.deviceId===device?.id)diagnostics.withCorrelation(record.ids,()=>diagnosticSpan('browser.credential',{operation:event==='onCompleted'?'credential.complete':'credential.failure',policyVersion:record.policyVersion,sessionId:record.sessionId},()=>audit(event==='onCompleted'?'vault.submission-completed':'vault.submission.failure',undefined,undefined,undefined,undefined,undefined,undefined,undefined,record))).catch(()=>{});
|
||||
if(!fileUploadGate.has(details.id))return;
|
||||
diagnosticSpan('browser.upload.release',{},()=>fileUploadGate.terminal(details.id)).catch(()=>{uploadLeaseFatal=true;quarantine('Upload protection cleanup failed. Restart the browser.');diagnosticSpan('browser.upload.fault',{decision:'block',reason:'upload-lease-release-failed'},()=>audit('upload.block','upload-lease-release-failed')).catch(()=>{});});
|
||||
});
|
||||
browserSession.webRequest.onBeforeRedirect({urls:['<all_urls>']},details=>credentialBroker?.redirect(details.id));
|
||||
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?{inspect:(data,pol)=>diagnosticSpan('browser.inspect.local',{bytes:data.length},()=>localInspector.inspect(data,pol)),scanDownload:(data,pol)=>diagnosticSpan('browser.yara',{operation:'scan',bytes:data.length},()=>localInspector.scanDownload(data,pol))}:null)),
|
||||
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;},
|
||||
@@ -510,10 +513,11 @@ function hooks(){
|
||||
const initialPolicy=policy,source=details.webContentsId>0?tabs.fromContents(details.webContentsId):null,sourceContext=source?tabContext(source.id,{foreground:false}):null;
|
||||
const sourceCurrent=()=>require('./request-source.cjs').requestSourceCurrent({details,source,token:sourceContext,registry:tabs,currentDocument:currentTabContext});let timer;
|
||||
// Async gates complete before Chromium may transmit the request.
|
||||
diagnosticSpan('browser.request',{requestType:details.resourceType,...(source?{sessionId:source.id}:{})},async()=>{try{
|
||||
requestDiagnostic(details,{requestType:details.resourceType,...(source?{sessionId:source.id}:{})},async()=>{try{
|
||||
timer=setTimeout(()=>{if(called)return;finish(true);audit('navigation.block','request-inspection-timeout',new URL(details.url).hostname).catch(()=>{});},40000);
|
||||
if(!ready||!policy||Date.parse(policy.expiresAt)<=Date.now()||!agentRequestOwnerAllowed(source)||(details.webContentsId>0&&(!source||!source.guarded))){if(ready&&policy&&(!agentRequestOwnerAllowed(source)||details.webContentsId>0&&(!source||!source.guarded)))await audit('navigation.block','invalid-source-owner',new URL(details.url).hostname);finish(true);return;}
|
||||
if(!core.destinationAllowed(details.url,policy)||(source?.security.agentAuthority&&!actorDestinationAllowed(details.url,source.security.agentAuthority))){audit('navigation.block',null,new URL(details.url).hostname);finish(true);return;}
|
||||
if(credentialBroker&&source){try{credentialBroker.guardIssued(details,credentialContext(source));credentialBroker.validateRedirect(details,credentialContext(source));}catch{await audit('vault.submission-denied');message='Login submission blocked by company policy. Contact your administrator.';emit();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.time<Date.now()-120000||recentRequests.size>2000)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(called||!ready||policy!==initialPolicy||!sourceCurrent()){if(!called&&ready&&policy===initialPolicy&&!sourceCurrent())await audit('navigation.block','request-source-context-changed',new URL(details.url).hostname);finish(true);return;}
|
||||
@@ -544,17 +548,21 @@ function hooks(){
|
||||
const requestFacts=require('../../packages/core/request-transfer-facts.cjs').requestFactsForTransfer(transfer);
|
||||
const releaseUpload=async()=>{if(called)return;await audit('upload.authorized',null,new URL(details.url).hostname,undefined,requestFacts);finish(!ready||policy!==initialPolicy||!sourceCurrent());};
|
||||
if(transfer.decision!=='allow'){await audit('upload.block',transfer.ruleId,new URL(details.url).hostname,undefined,requestFacts);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;}
|
||||
let claim;
|
||||
try{claim=credentialBroker&&source?await diagnosticSpan('browser.credential',{operation:'credential.claim'},()=>credentialBroker.claim(details,requestBytes,credentialContext(source))):null;if(claim){for(const [id,row]of credentialRequestCorrelations)if(!row.released&&row.expires<=Date.now())credentialRequestCorrelations.delete(id);if(credentialRequestCorrelations.size>=256){credentialBroker.cancel(credentialContext(source));throw Error('Controlled login audit capacity reached');}credentialRequestCorrelations.set(details.id,{ids:diagnosticCorrelation(),policyVersion:initialPolicy.version,deviceId:device.id,sessionId:source.id,expires:Date.now()+60000,released:false});}}catch{requestBytes.fill(0);claim?.residual.fill(0);await audit('vault.submission-denied');message='Login submission blocked by company policy. Contact your administrator.';emit();finish(true);return;}
|
||||
try{const scanBytes=claim?claim.residual:requestBytes;
|
||||
const verdict=core.inspect(scanBytes,policy);requestBytes.fill(0);
|
||||
if(verdict.decision==='block'&&verdict.ruleId!=='uninspectable'){claim?.residual.fill(0);const reference=await audit('dlp.block',verdict.ruleId,new URL(details.url).hostname);if(claim)await audit('vault.submission-denied');if(details.resourceType==='mainFrame'&&details.method==='POST'){message='Submission blocked by company data policy.'+(reference?' Event: '+reference:'');emit();}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;
|
||||
if(needsLocal&&!localInspector){claim?.residual.fill(0);audit('dlp.block','inspector-unavailable',new URL(details.url).hostname);finish(true);return;}
|
||||
const snapshot=claim?Buffer.from(claim.residual):Buffer.concat(uploads.map(p=>Buffer.from(p.bytes))),currentPolicy=policy;claim?.residual.fill(0);
|
||||
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=called||!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;}
|
||||
}else{claim?.residual.fill(0);await releaseUpload();return;}
|
||||
}finally{requestBytes.fill(0);claim?.residual.fill(0);}
|
||||
}
|
||||
finish(false);
|
||||
}catch{finish(true);}finally{clearTimeout(timer);}}).catch(()=>finish(true));
|
||||
@@ -566,10 +574,12 @@ function hooks(){
|
||||
const sourceCurrent=()=>require('./request-source.cjs').requestSourceCurrent({details,source,token:sourceContext,registry:tabs,currentDocument:currentTabContext});
|
||||
const current=()=>!called&&ready&&policy===initialPolicy&&Date.parse(initialPolicy?.expiresAt)>Date.now()&&sourceCurrent();
|
||||
let timer;
|
||||
diagnosticSpan('browser.request',{requestType:details.resourceType,...(source?{sessionId:source.id}:{})},async()=>{
|
||||
requestDiagnostic(details,{requestType:details.resourceType,...(source?{sessionId:source.id}:{})},async()=>{
|
||||
timer=setTimeout(()=>{if(called)return;finish(true);audit('dlp.block','headers-inspection-timeout',new URL(details.url).hostname).catch(()=>{});},40000);
|
||||
try{
|
||||
if(!current()||!agentRequestOwnerAllowed(source)||(details.webContentsId>0&&(!source||!source.guarded))||!core.destinationAllowed(details.url,initialPolicy)||(source?.security.agentAuthority&&!actorDestinationAllowed(details.url,source.security.agentAuthority))){if(ready&&policy===initialPolicy&&(!agentRequestOwnerAllowed(source)||details.webContentsId>0&&(!source||!source.guarded)))await audit('navigation.block','invalid-source-owner',new URL(details.url).hostname);finish(true);return;}
|
||||
const controlled=credentialBroker?.has(details.id)===true;
|
||||
if(credentialBroker&&source)await diagnosticSpan('browser.credential',{operation:controlled?'credential.headers':'credential.redirect'},()=>{if(controlled)return credentialBroker.validateHeaders(details,credentialContext(source));credentialBroker.guardIssued(details,credentialContext(source));return credentialBroker.validateRedirect(details,credentialContext(source));});
|
||||
const {snapshotHeaders,inspectHeaders}=require('./header-inspection.cjs');
|
||||
let snapshot;try{snapshot=snapshotHeaders(details.requestHeaders,initialPolicy);}catch(error){await audit('dlp.block',error.message==='headers-size-limit'?'headers-size-limit':'headers-uninspectable',new URL(details.url).hostname);finish(true);return;}
|
||||
const verdict=await diagnosticSpan('browser.request.headers',{bytes:snapshot.bytes,count:snapshot.count,requestType:details.resourceType},()=>inspectHeaders(snapshot,{policy:initialPolicy,inspect:core.inspect,isCurrent:current,
|
||||
@@ -577,6 +587,7 @@ function hooks(){
|
||||
inspectEnterprise:(bytes,p)=>diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanEnterprise(bytes,p,'upload'))}));
|
||||
if(!current()){if(ready&&policy===initialPolicy&&!sourceCurrent())await audit('navigation.block','header-source-context-changed',new URL(details.url).hostname);finish(true);return;}
|
||||
if(verdict.decision!=='allow'){await audit('dlp.block',verdict.ruleId||'headers-inspection-incomplete',new URL(details.url).hostname);finish(true);return;}
|
||||
if(controlled){await audit('vault.submission-authorized');if(!current())throw Error('Controlled login state changed');diagnostics.spanSync('browser.credential',diagnosticContext({operation:'credential.authorize'}),()=>{if(!current())throw Error('Controlled login state changed');credentialBroker.finalize(details.id,credentialContext(source),details);finish(false,snapshot.headers);const record=credentialRequestCorrelations.get(details.id);if(record)record.released=true;});return;}
|
||||
// Chromium receives the exact immutable snapshot that was approved.
|
||||
finish(false,snapshot.headers);
|
||||
}catch{if(current())await audit('dlp.block','headers-inspection-incomplete',new URL(details.url).hostname);finish(true);}
|
||||
@@ -647,6 +658,35 @@ function updateManagedAppearance(input){requireManagedSection('appearance');appe
|
||||
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(libraryOrganizer?.isBusy())throw Error('Library organization is running');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();}
|
||||
let sessionSnapshotStore;
|
||||
let credentialBroker;
|
||||
const credentialRequestCorrelations=new Map();
|
||||
function requestDiagnostic(details,attributes,fn){const record=credentialRequestCorrelations.get(details.id);return record?diagnostics.withCorrelation(record.ids,()=>diagnosticSpan('browser.request',{...attributes,policyVersion:record.policyVersion},fn)):diagnosticSpan('browser.request',attributes,fn);}
|
||||
function credentialContext(tab){
|
||||
const contents=tab.contents,authority=tab.security.agentAuthority,actor=authority?.principalId?{agentId:authority.principalId,generation:authority.generation}:tab.security.credentialActor||{agentId:'human-ui',generation:0};
|
||||
return {ready,policy,token:tabContext(tab.id),sessionId:tab.id,webContents:contents,mainFrame:contents.mainFrame,sourceURL:contents.getURL(),actor,vault};
|
||||
}
|
||||
function credentialAuthorityCurrent(context,{phase},details){
|
||||
try{
|
||||
const tab=tabs.get(context.sessionId),contents=tab.contents,live=tabContext(tab.id),settings=managedSettings();
|
||||
if(!ready||policy!==context.policy||Date.parse(policy.expiresAt)<=Date.now()||!policy.passwordManager.enabled||!policy.passwordManager.allowAutofill||!policy.passwordManager.controlledSubmission.enabled||!featureAllowed(settings,'passwords')||tab!==activeTab()||contents!==context.webContents||contents.isDestroyed()||contents.mainFrame!==context.mainFrame||contents.mainFrame.processId!==context.frameProcessId||contents.mainFrame.routingId!==context.frameRoutingId||contents.getURL()!==context.sourceURL||live.activation!==context.token.activation||live.paused||vault!==context.vault||vault?.status().locked)return false;
|
||||
if(JSON.stringify(credentialContext(tab).actor)!==JSON.stringify(context.actor))return false;
|
||||
if(context.actor.agentId!=='human-ui'&&!actorGrantAllowed(context.authority,'vault:prepare-submit',details?.url||context.sourceURL))return false;
|
||||
if(phase==='issued')return currentTabContext(context.token);
|
||||
if(phase==='watch')return tab.security.interactionDocumentGeneration===context.documentGeneration||tab.security.interactionDocumentGeneration===context.documentGeneration+1;
|
||||
return phase==='redirect'?tab.security.interactionDocumentGeneration>=context.documentGeneration+1:tab.security.interactionDocumentGeneration===context.documentGeneration+1;
|
||||
}catch{return false;}
|
||||
}
|
||||
function getCredentialBroker(){return credentialBroker??=require('./credential-submission.cjs').createCredentialSubmissionBroker({isCurrent:credentialAuthorityCurrent,traceOperation:(operation,fn)=>diagnostics.spanSync('browser.credential',diagnosticContext({operation}),fn)});}
|
||||
async function armCredentialSubmission({context,formId,credential,vault:current,vaultGeneration}){
|
||||
const tab=tabs.get(context.sessionId||context.token.tabId),actor=context.actor||{agentId:'human-ui',generation:0};tab.security.credentialActor=Object.freeze({...actor});
|
||||
const snapshot={...credentialContext(tab),documentGeneration:tab.security.interactionDocumentGeneration,frameProcessId:tab.contents.mainFrame.processId,frameRoutingId:tab.contents.mainFrame.routingId,vault:current,authority:actor.agentId==='human-ui'?undefined:actor.agentId==='legacy-paired-agent'?{action:'vault:prepare-submit'}:{principalId:actor.agentId,generation:actor.generation,action:'vault:prepare-submit'}};
|
||||
const result=await diagnosticSpan('browser.credential',{operation:'credential.issue',sessionId:tab.id},()=>getCredentialBroker().issue({context:snapshot,formId,username:credential.username,password:credential.password,vault:current,vaultGeneration}));
|
||||
await audit('vault.submission-prepared');if(!credentialAuthorityCurrent(snapshot,{phase:'issued'})){getCredentialBroker().cancel(snapshot);throw Error('Controlled login state changed');}return {...result,cancel:()=>getCredentialBroker().cancel(snapshot)};
|
||||
}
|
||||
async function handoffVault(){
|
||||
const token=require('node:crypto').randomUUID();
|
||||
await new Promise((resolve,reject)=>{const handler=(event,value)=>{if(win&&!win.isDestroyed()&&event.sender===win.webContents&&event.senderFrame===win.webContents.mainFrame&&event.senderFrame?.url===uiUrl&&value===token){clearTimeout(timer);ipcMain.removeListener('managed:vault-handoff-ack',handler);resolve();}};const timer=setTimeout(()=>{ipcMain.removeListener('managed:vault-handoff-ack',handler);reject(Error('Trusted vault interface unavailable'));},1500);ipcMain.on('managed:vault-handoff-ack',handler);win.webContents.send('managed:vault-handoff',token);});
|
||||
}
|
||||
function agentSessionControls(){
|
||||
requireEncryption();sessionSnapshotStore??=require('./session-snapshot.cjs').createSessionSnapshotStore({filePath:path.join(app.getPath('userData'),'managed-sessions.bin'),protect:bytes=>safeStorage.encryptString(bytes.toString('base64')),unprotect:bytes=>Buffer.from(safeStorage.decryptString(bytes),'base64')});
|
||||
const sessionContext=id=>{const context=agentContext(id),actor=agentActorStorage.getStore();return {...context,ready:context.ready&&actorGrantAllowed(actor,actor?.action),actor:context.actor||Object.freeze({agentId:'legacy-paired-agent',generation:1})};};
|
||||
@@ -663,6 +703,7 @@ function agentAppControls(){
|
||||
const vaultControls=require('./vault-agent-controls.cjs').createVaultAgentControls({
|
||||
getContext:id=>{const context=agentContext(id);return {...context,ready:context.ready&&featureAllowed(managedSettings(),'passwords'),actor:context.actor||Object.freeze({agentId:'legacy-paired-agent',generation:1})};},
|
||||
contextCurrent:currentTabContext,getVault:requireVault,checkVisual:context=>checkVisual(tabs.get(context.sessionId)),inspect:inspectManagedAgentText,
|
||||
beforeFill:handoffVault,afterFill:()=>{vaultPanel=false;emit();},prepareSubmission:armCredentialSubmission,
|
||||
audit:event=>audit(event.action),traceOperation:(operation,fn)=>diagnosticSpan('browser.vault',{operation},fn)
|
||||
});
|
||||
const checked=(operation)=>async(args,{signal,policyVersion}={})=>{
|
||||
@@ -673,6 +714,7 @@ function agentAppControls(){
|
||||
'vault:status':checked((args,options)=>vaultControls.status(args,options)),
|
||||
'vault:list':checked((args,options)=>vaultControls.list(args,options)),
|
||||
'vault:fill':checked((args,options)=>vaultControls.fill(args,options)),
|
||||
'vault:prepare-submit':checked((args,options)=>vaultControls.prepare(args,options)),
|
||||
'window:state':checked((args,options)=>windowControls.state(args,options)),
|
||||
'window:control':checked((args,options)=>windowControls.control(args,options)),
|
||||
'application:state':checked(()=>({ready,policyVersion:policy.version,activeTabId:activeTab()?.id,tabCount:tabs.all().length})),
|
||||
@@ -798,25 +840,32 @@ app.whenReady().then(async()=>{
|
||||
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 fillManagedVault=async(id,controlled=false)=>{
|
||||
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');
|
||||
if(controlled&&!initial.passwordManager.controlledSubmission.enabled)throw Error('Controlled login submission is disabled by company policy');
|
||||
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');
|
||||
await handoffVault();if(!currentTabContext(context)||policy!==initial||current.status().locked)throw Error('Browser state changed');tab.security.agentAuthority=null;tab.security.credentialActor=Object.freeze({agentId:'human-ui',generation:0});
|
||||
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));
|
||||
const code=controlled?require('./password-fill.cjs').prepareFillScript(url,credential,initial.passwordManager.controlledSubmission.forms,Date.parse(initial.expiresAt)):fillScript(url,credential,Date.parse(initial.expiresAt));
|
||||
const result=await diagnosticSpan('browser.password.fill',{sessionId:context.tabId},()=>contents.executeJavaScriptInIsolatedWorld(999,[{code}],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.');}
|
||||
await audit('vault.filled');if(!currentTabContext(context)||policy!==initial||current.status().locked)throw Error('Browser state changed');
|
||||
if(controlled)await armCredentialSubmission({context:{sessionId:tab.id,token:context},formId:result.formId,credential,vault:current,vaultGeneration:current.accessGeneration()});
|
||||
vaultPanel=false;emit();return {completed:true,...(controlled?{prepared:true}:{})};
|
||||
}catch{credentialBroker?.revoke();vaultPanel=false;emit();throw Error('Cannot fill this login. Unlock the vault and use its exact approved HTTPS origin with a single visible same-origin POST login form.');}
|
||||
finally{if(credential){credential.username='';credential.password='';}}
|
||||
});
|
||||
};
|
||||
ipc('managed:vault-fill',id=>fillManagedVault(id));
|
||||
ipc('managed:vault-prepare-submit',id=>fillManagedVault(id,true));
|
||||
ipc('managed:ai',managedAI);
|
||||
ipc('managed:agent-connection',async principalId=>{
|
||||
requireFeatureSection('agents');
|
||||
@@ -849,7 +898,7 @@ app.whenReady().then(async()=>{
|
||||
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();});
|
||||
win.on('resize',layout);win.on('maximize',emit);win.on('unmaximize',emit);win.on('blur',lockVault);win.on('minimize',lockVault);win.on('closed',()=>{credentialBroker?.close();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=require('../../packages/core/request-transfer-facts.cjs').migrateQueuedRequestFacts(JSON.parse(safeStorage.decryptString(fs.readFileSync(queuePath))));persistAudit();}if(device){await sync();await audit('browser.started');}}catch(e){quarantine(`Enrollment could not load: ${e.message}`);}
|
||||
|
||||
Reference in new issue
Block a user