'use strict'; const {safeFilename}=require('../../packages/core/file-transfer.cjs'); const credentialPatterns=[/-----BEGIN (?:[A-Z0-9 ]*PRIVATE KEY|OPENSSH PRIVATE KEY)-----/i,/\b(?:bearer|basic)\s+[A-Za-z0-9+/_=-]{8,}/i,/\b(?:password|passwd|pwd|api[ _-]?key|access[ _-]?token|refresh[ _-]?token|client[ _-]?secret|authorization)\b["']?\s*[:=]\s*["']?[^\s"',;]{4,}/i,/\bsk-[A-Za-z0-9_-]{16,}\b/,/\bgh[pousr]_[A-Za-z0-9]{20,}\b/,/\bgithub_pat_[A-Za-z0-9_]{20,}\b/,/\bAKIA[A-Z0-9]{16}\b/,/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/]; function validateDownloadEvidence(value){ if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).some(k=>!['sourceUrl','name','sha256','bytes','mime','decision','ruleId'].includes(k)))return false; if(typeof value.sourceUrl!=='string'||value.sourceUrl.length>4096||typeof value.name!=='string'||!value.name.length||value.name.length>255||typeof value.mime!=='string'||value.mime.length>100||!/^[a-f0-9]{64}$/.test(value.sha256)||!Number.isSafeInteger(value.bytes)||value.bytes<0||!['allow','block'].includes(value.decision))return false; if(value.ruleId!==undefined&&(typeof value.ruleId!=='string'||value.ruleId.length>100))return false; try{const u=new URL(value.sourceUrl);if(!['http:','https:'].includes(u.protocol)||u.username||u.password||u.search||u.hash)return false;const fields=[value.name,value.sourceUrl,decodeURIComponent(u.pathname)];if(fields.some(v=>credentialPatterns.some(p=>p.test(v))))return false;}catch{return false;} return true; } // Only bounded file metadata is captured. The document itself never enters the // forensic queue; main persists this beside its source audit event atomically. function buildDownloadEvidence({url,name,mime,facts,decision,ruleId},policy){ if(policy?.evidence?.enabled!==true||policy.evidence.captureDownloads!==true)return null; if(!facts||!Number.isSafeInteger(facts.size)||facts.size<0||!/^[a-f0-9]{64}$/.test(facts.sha256)||!['allow','block'].includes(decision))return null; let source;try{source=new URL(url);if(!['http:','https:'].includes(source.protocol))return null;source.username='';source.password='';source.search='';source.hash='';}catch{return null;} if(source.href.length>4096)return null; const result={sourceUrl:source.href,name:safeFilename(name),sha256:facts.sha256,bytes:facts.size,mime:String(mime||'').split(';',1)[0].trim().toLowerCase().replace(/[\x00-\x1f\x7f]/g,'').slice(0,100),decision}; if(typeof ruleId==='string'&&ruleId.length<=100&&!/[\x00-\x1f\x7f]/.test(ruleId))result.ruleId=ruleId; return validateDownloadEvidence(result)?result:null; } const {InteractionEvidenceSchema}=require('../../packages/core/evidence-payload.cjs'); function validateInteractionEvidence(value){ if(!InteractionEvidenceSchema.safeParse(value).success)return false; try{const u=new URL(value.pageUrl);return !credentialPatterns.some(p=>p.test(value.pageUrl)||p.test(decodeURIComponent(u.pathname)));}catch{return false;} } function buildInteractionEvidence({action,sessionId,url,target,sensitiveField,outcome,bytes,captureSource},policy){ if(policy?.evidence?.enabled!==true||policy.evidence.captureInteractions!==true||sensitiveField!==false||!['completed','observed'].includes(outcome))return null; let page;try{page=new URL(url);if(!['http:','https:'].includes(page.protocol))return null;}catch{return null;} if(target!==undefined){ if(!target||typeof target!=='object'||Array.isArray(target)||Object.keys(target).some(key=>!['tag','role','x','y'].includes(key))||typeof target.tag!=='string')return null; // Native adapters expose bounded lowercase tags. Preserve their meaning in // the canonical evidence representation without accepting arbitrary DOM text. target={...target,tag:target.tag.toUpperCase()}; } // Paths can carry identifiers or credentials. Automatic metadata uses origin // only; detailed evidence remains an explicitly governed separate workflow. const payload={pageUrl:page.origin+'/',action,sessionId,outcome,sensitiveField:false,captureSource,...(target===undefined?{}:{target}),...(bytes===undefined?{}:{bytes})}; return validateInteractionEvidence(payload)?structuredClone(payload):null; } module.exports={buildDownloadEvidence,validateDownloadEvidence,buildInteractionEvidence,validateInteractionEvidence};