2026-10-13 16:58:15 +02:00
|
|
|
'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;
|
2026-10-13 23:19:22 +02:00
|
|
|
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
|
2026-10-13 16:58:15 +02:00
|
|
|
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};
|
2026-10-13 23:19:22 +02:00
|
|
|
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
|
2026-10-13 16:58:15 +02:00
|
|
|
if(typeof ruleId==='string'&&ruleId.length<=100&&!/[\x00-\x1f\x7f]/.test(ruleId))result.ruleId=ruleId;
|
|
|
|
|
return validateDownloadEvidence(result)?result:null;
|
|
|
|
|
}
|
2026-10-13 18:04:02 +02:00
|
|
|
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){
|
2026-10-13 18:24:44 +02:00
|
|
|
if(policy?.evidence?.enabled!==true||policy.evidence.captureInteractions!==true||sensitiveField!==false||!['completed','observed'].includes(outcome))return null;
|
2026-10-13 18:04:02 +02:00
|
|
|
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.
|
2026-10-13 18:24:44 +02:00
|
|
|
const payload={pageUrl:page.origin+'/',action,sessionId,outcome,sensitiveField:false,captureSource,...(target===undefined?{}:{target}),...(bytes===undefined?{}:{bytes})};
|
2026-10-13 18:04:02 +02:00
|
|
|
return validateInteractionEvidence(payload)?structuredClone(payload):null;
|
|
|
|
|
}
|
|
|
|
|
module.exports={buildDownloadEvidence,validateDownloadEvidence,buildInteractionEvidence,validateInteractionEvidence};
|