Services: - ja4sentinel: TLS/JA4 fingerprint capture daemon (Go, libpcap) - logcorrelator: JA4 log correlation engine (Go, ClickHouse) - mod_reqin_log: Apache module (C, JSON request logging) - bot_detector: ML bot detection pipeline (Python) - dashboard: FastAPI/Streamlit analytics UI (Python) Shared libraries: - shared/go/ja4common: logger, config, shutdown, ipfilter (Go module) - shared/python/ja4_common: ClickHouseClient, ClickHouseSettings (Python package) - shared/clickhouse/: canonical SQL migrations (10 files) Build & packaging: - Unified 3-stage Dockerfile.package for Go RPMs (el8/el9/el10) - go.work workspace linking sentinel, correlator, ja4common - Makefile with test-all, build-all, rpm-* targets Fixes applied: - go.work: 1.21 → 1.24.6 (required by sentinel) - correlator Dockerfiles: golang:1.21 → golang:1.24 - replace directives in go.mod for ja4common local path - pyproject.toml: setuptools.backends → setuptools.build_meta - Removed static libpcap linking (unavailable on Rocky 9) - Fixed data races in output/writers_test.go (sync.Mutex + atomic.Int32) - Rewrote corrupted test files (logger_test.go × 2) Test coverage: - correlator: 67.1% total (unixsocket 80.5%, config 91.7%, app 83.3%, multi 87.7%, stdout 100%) - sentinel: all 10 packages pass (api, capture, config, fingerprint, ipfilter, logging, output, tlsparse) Documentation: - README.md + docs/ (architecture, development, 5 services, shared libs, DB schema & migrations) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
296 lines
11 KiB
TypeScript
296 lines
11 KiB
TypeScript
import { useState } from 'react';
|
||
import { PREDEFINED_TAGS } from '../utils/classifications';
|
||
|
||
interface BulkClassificationProps {
|
||
selectedIPs: string[];
|
||
onClose: () => void;
|
||
onSuccess: () => void;
|
||
}
|
||
|
||
export function BulkClassification({ selectedIPs, onClose, onSuccess }: BulkClassificationProps) {
|
||
const [selectedLabel, setSelectedLabel] = useState<string>('suspicious');
|
||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||
const [comment, setComment] = useState('');
|
||
const [confidence, setConfidence] = useState(0.7);
|
||
const [processing, setProcessing] = useState(false);
|
||
const [progress, setProgress] = useState({ current: 0, total: selectedIPs.length });
|
||
|
||
const toggleTag = (tag: string) => {
|
||
setSelectedTags(prev =>
|
||
prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]
|
||
);
|
||
};
|
||
|
||
const handleBulkClassify = async () => {
|
||
setProcessing(true);
|
||
try {
|
||
// Process in batches of 10
|
||
const batchSize = 10;
|
||
for (let i = 0; i < selectedIPs.length; i += batchSize) {
|
||
const batch = selectedIPs.slice(i, i + batchSize);
|
||
|
||
await Promise.all(
|
||
batch.map(ip =>
|
||
fetch('/api/analysis/classifications', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
ip,
|
||
label: selectedLabel,
|
||
tags: selectedTags,
|
||
comment: `${comment} (Classification en masse - ${selectedIPs.length} IPs)`,
|
||
confidence,
|
||
analyst: 'soc_user',
|
||
bulk_operation: true,
|
||
bulk_id: `bulk-${Date.now()}`
|
||
})
|
||
})
|
||
)
|
||
);
|
||
|
||
setProgress({ current: Math.min(i + batchSize, selectedIPs.length), total: selectedIPs.length });
|
||
}
|
||
|
||
// Log the bulk operation
|
||
await fetch('/api/audit/logs', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
action: 'BULK_CLASSIFICATION',
|
||
entity_type: 'ip',
|
||
entity_count: selectedIPs.length,
|
||
details: {
|
||
label: selectedLabel,
|
||
tags: selectedTags,
|
||
confidence
|
||
}
|
||
})
|
||
});
|
||
|
||
onSuccess();
|
||
} catch (error) {
|
||
console.error('Bulk classification error:', error);
|
||
alert('Erreur lors de la classification en masse');
|
||
} finally {
|
||
setProcessing(false);
|
||
}
|
||
};
|
||
|
||
const handleExportCSV = () => {
|
||
const csv = selectedIPs.map(ip =>
|
||
`${ip},${selectedLabel},"${selectedTags.join(';')}",${confidence},"${comment}"`
|
||
).join('\n');
|
||
|
||
const header = 'ip,label,tags,confidence,comment\n';
|
||
const blob = new Blob([header + csv], { type: 'text/csv' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `bulk_classification_${Date.now()}.csv`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||
<div className="bg-background-secondary rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div>
|
||
<h2 className="text-xl font-bold text-text-primary">
|
||
🏷️ Classification en Masse
|
||
</h2>
|
||
<p className="text-sm text-text-secondary mt-1">
|
||
{selectedIPs.length} IPs sélectionnées
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="text-text-secondary hover:text-text-primary"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
{/* Progress Bar */}
|
||
{processing && (
|
||
<div className="mb-6">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-sm text-text-secondary">Progression</span>
|
||
<span className="text-sm text-text-primary font-bold">
|
||
{progress.current} / {progress.total}
|
||
</span>
|
||
</div>
|
||
<div className="w-full bg-background-card rounded-full h-3">
|
||
<div
|
||
className="h-3 rounded-full bg-accent-primary transition-all"
|
||
style={{ width: `${(progress.current / progress.total) * 100}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Classification Label */}
|
||
<div className="mb-6">
|
||
<label className="text-sm font-semibold text-text-primary mb-3 block">
|
||
Niveau de Menace
|
||
</label>
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<button
|
||
onClick={() => setSelectedLabel('legitimate')}
|
||
disabled={processing}
|
||
className={`py-4 px-4 rounded-lg font-medium transition-colors ${
|
||
selectedLabel === 'legitimate'
|
||
? 'bg-threat-low text-white ring-2 ring-threat-low'
|
||
: 'bg-background-card text-text-secondary hover:text-text-primary'
|
||
}`}
|
||
>
|
||
<div className="text-2xl mb-1">✅</div>
|
||
<div className="text-sm">Légitime</div>
|
||
</button>
|
||
<button
|
||
onClick={() => setSelectedLabel('suspicious')}
|
||
disabled={processing}
|
||
className={`py-4 px-4 rounded-lg font-medium transition-colors ${
|
||
selectedLabel === 'suspicious'
|
||
? 'bg-threat-medium text-white ring-2 ring-threat-medium'
|
||
: 'bg-background-card text-text-secondary hover:text-text-primary'
|
||
}`}
|
||
>
|
||
<div className="text-2xl mb-1">⚠️</div>
|
||
<div className="text-sm">Suspect</div>
|
||
</button>
|
||
<button
|
||
onClick={() => setSelectedLabel('malicious')}
|
||
disabled={processing}
|
||
className={`py-4 px-4 rounded-lg font-medium transition-colors ${
|
||
selectedLabel === 'malicious'
|
||
? 'bg-threat-high text-white ring-2 ring-threat-high'
|
||
: 'bg-background-card text-text-secondary hover:text-text-primary'
|
||
}`}
|
||
>
|
||
<div className="text-2xl mb-1">❌</div>
|
||
<div className="text-sm">Malveillant</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tags */}
|
||
<div className="mb-6">
|
||
<label className="text-sm font-semibold text-text-primary mb-3 block">
|
||
Tags
|
||
</label>
|
||
<div className="flex flex-wrap gap-2 max-h-40 overflow-y-auto p-2 bg-background-card rounded-lg">
|
||
{PREDEFINED_TAGS.map(tag => (
|
||
<button
|
||
key={tag}
|
||
onClick={() => toggleTag(tag)}
|
||
disabled={processing}
|
||
className={`px-3 py-1.5 rounded text-xs transition-colors ${
|
||
selectedTags.includes(tag)
|
||
? 'bg-accent-primary text-white'
|
||
: 'bg-background-secondary text-text-secondary hover:text-text-primary'
|
||
} disabled:opacity-50`}
|
||
>
|
||
{tag}
|
||
</button>
|
||
))}
|
||
</div>
|
||
{selectedTags.length > 0 && (
|
||
<div className="mt-2 text-xs text-text-secondary">
|
||
{selectedTags.length} tag(s) sélectionné(s)
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Confidence Slider */}
|
||
<div className="mb-6">
|
||
<label className="text-sm font-semibold text-text-primary mb-3 block">
|
||
Confiance: {(confidence * 100).toFixed(0)}%
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="1"
|
||
step="0.1"
|
||
value={confidence}
|
||
onChange={(e) => setConfidence(parseFloat(e.target.value))}
|
||
disabled={processing}
|
||
className="w-full h-2 bg-background-card rounded-lg appearance-none cursor-pointer"
|
||
/>
|
||
<div className="flex justify-between text-xs text-text-secondary mt-1">
|
||
<span>0%</span>
|
||
<span>50%</span>
|
||
<span>100%</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Comment */}
|
||
<div className="mb-6">
|
||
<label className="text-sm font-semibold text-text-primary mb-3 block">
|
||
Commentaire
|
||
</label>
|
||
<textarea
|
||
value={comment}
|
||
onChange={(e) => setComment(e.target.value)}
|
||
disabled={processing}
|
||
placeholder="Notes d'analyse..."
|
||
className="w-full bg-background-card border border-background-card rounded-lg p-3 text-text-primary placeholder-text-disabled focus:outline-none focus:border-accent-primary"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
|
||
{/* Summary */}
|
||
<div className="bg-background-card rounded-lg p-4 mb-6">
|
||
<div className="text-sm font-semibold text-text-primary mb-2">
|
||
📋 Résumé
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||
<div>
|
||
<span className="text-text-secondary">IPs:</span>{' '}
|
||
<span className="text-text-primary font-bold">{selectedIPs.length}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-text-secondary">Label:</span>{' '}
|
||
<span className={`font-bold ${
|
||
selectedLabel === 'legitimate' ? 'text-threat-low' :
|
||
selectedLabel === 'suspicious' ? 'text-threat-medium' :
|
||
'text-threat-high'
|
||
}`}>
|
||
{selectedLabel.toUpperCase()}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-text-secondary">Tags:</span>{' '}
|
||
<span className="text-text-primary">{selectedTags.length}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-text-secondary">Confiance:</span>{' '}
|
||
<span className="text-text-primary">{(confidence * 100).toFixed(0)}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={handleExportCSV}
|
||
disabled={processing}
|
||
className="flex-1 py-3 px-4 bg-background-card text-text-primary rounded-lg font-medium hover:bg-background-card/80 transition-colors disabled:opacity-50"
|
||
>
|
||
📄 Export CSV
|
||
</button>
|
||
<button
|
||
onClick={handleBulkClassify}
|
||
disabled={processing || !selectedLabel}
|
||
className="flex-1 py-3 px-4 bg-accent-primary text-white rounded-lg font-medium hover:bg-accent-primary/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{processing ? '⏳ Traitement...' : `💾 Classifier ${selectedIPs.length} IPs`}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|