Files
safetynet/apps/browser/evidence-capture.cjs
T

23 lines
2.6 KiB
JavaScript

'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;
}
module.exports={buildDownloadEvidence,validateDownloadEvidence};