119 lines
4.7 KiB
JavaScript
119 lines
4.7 KiB
JavaScript
|
|
/**
|
||
|
|
* 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;
|