Files
boc/aamos-api-v1/api/v1/score.mjs
T

119 lines
4.7 KiB
JavaScript
Raw Normal View History

/**
* 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;