feat: ja4-platform monorepo — 5 services unified, tests & RPM builds standardized

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>
This commit is contained in:
toto
2026-04-07 16:42:59 +02:00
commit d469e39da7
278 changed files with 1621301 additions and 0 deletions

View File

@ -0,0 +1,295 @@
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>
);
}