/** * POST /v1/score — Riskpoängsättning * Beräknar riskpoäng baserat på bildanalys + metadata */ import { Router } from 'express'; import sharp from 'sharp'; import { fetchImage, hashInput, saveResult, genReqId, requireAuth } from './utils.mjs'; const router = Router(); async function analyzeImageRisk(buffer) { const { data, info } = await sharp(buffer).resize(128, 128).raw().toBuffer({ resolveWithObject: true }); const w = info.width, h = info.height; // Analyze image quality metrics let totalBrightness = 0, totalContrast = 0, edgeCount = 0; const brightnessHistogram = new Array(256).fill(0); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const i = (y * w + x) * 3; const gray = (data[i] + data[i+1] + data[i+2]) / 3; totalBrightness += gray; brightnessHistogram[Math.round(gray)]++; if (x < w - 1) { const diff = Math.abs(gray - ((data[i+3] + data[i+4] + data[i+5]) / 3)); totalContrast += diff; if (diff > 30) edgeCount++; } } } const pixelCount = w * h; const avgBrightness = totalBrightness / pixelCount; const avgContrast = totalContrast / pixelCount; const edgeRatio = edgeCount / pixelCount; // Calculate entropy (measure of randomness/complexity) let entropy = 0; for (let i = 0; i < 256; i++) { const p = brightnessHistogram[i] / pixelCount; if (p > 0) entropy -= p * Math.log2(p); } return { brightness: Math.round(avgBrightness), contrast: Math.round(avgContrast), edge_ratio: parseFloat(edgeRatio.toFixed(4)), entropy: parseFloat(entropy.toFixed(4)), dark_ratio: parseFloat((brightnessHistogram.slice(0, 50).reduce((a,b)=>a+b,0) / pixelCount).toFixed(4)), bright_ratio: parseFloat((brightnessHistogram.slice(200).reduce((a,b)=>a+b,0) / pixelCount).toFixed(4)), }; } function calculateRiskScore(analysis, metadata = {}) { let score = 0.3; // Base risk const factors = []; // Image quality risks if (analysis.brightness < 30) { score += 0.15; factors.push({ type: 'image', reason: 'Very dark image', contribution: 0.15 }); } if (analysis.brightness > 240) { score += 0.1; factors.push({ type: 'image', reason: 'Overexposed image', contribution: 0.1 }); } if (analysis.contrast < 5) { score += 0.2; factors.push({ type: 'image', reason: 'Low contrast — possible synthetic image', contribution: 0.2 }); } if (analysis.entropy < 3) { score += 0.15; factors.push({ type: 'image', reason: 'Low entropy — possible compression artifact or synthetic', contribution: 0.15 }); } if (analysis.edge_ratio < 0.02) { score += 0.1; factors.push({ type: 'image', reason: 'Few edges — possibly blurred or artificial', contribution: 0.1 }); } // Metadata risks if (metadata.source === 'base64' && !metadata.filename) { score += 0.05; factors.push({ type: 'metadata', reason: 'No filename metadata', contribution: 0.05 }); } if (metadata.user_agent && metadata.user_agent.includes('bot')) { score += 0.1; factors.push({ type: 'metadata', reason: 'Bot user agent', contribution: 0.1 }); } // Time-based risks const hour = new Date().getUTCHours(); if (hour < 5 || hour > 22) { score += 0.05; factors.push({ type: 'time', reason: 'Unusual request hour', contribution: 0.05 }); } return { score: parseFloat(Math.min(1.0, score).toFixed(4)), factors: factors.slice(0, 5), analysis, }; } router.post('/', requireAuth, async (req, res) => { const requestId = genReqId(); const start = Date.now(); try { const { image_url, image_base64, metadata = {} } = req.body || {}; const img = await fetchImage({ image_url, image_base64 }); const inputHash = hashInput(img.buffer); const analysis = await analyzeImageRisk(img.buffer); const risk = calculateRiskScore(analysis, { ...metadata, source: img.source }); const result = { ok: true, endpoint: 'score', request_id: requestId, risk_score: risk.score, risk_level: risk.score > 0.7 ? 'high' : risk.score > 0.4 ? 'medium' : 'low', confidence: parseFloat((1 - risk.score * 0.3).toFixed(4)), factors: risk.factors, image_analysis: risk.analysis, recommendations: risk.score > 0.7 ? ['Require additional verification', 'Flag for manual review', 'Check device fingerprint'] : risk.score > 0.4 ? ['Standard verification recommended', 'Monitor for anomalies'] : ['Low risk — standard processing'], inference_time_ms: Date.now() - start, }; await saveResult('score', requestId, inputHash, result, result.confidence, { source: img.source, metadata }); res.json(result); } catch (e) { console.error('[score]', e); res.status(500).json({ ok: false, error: e.message, request_id: requestId }); } }); export default router;