|
|
|
@@ -0,0 +1,567 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="h-screen flex flex-col bg-monokai-bg">
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<header class="bg-monokai-bg border-b-2 border-monokai-comment p-4">
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
<h1 class="text-3xl font-bold text-monokai-green">IPWatch</h1>
|
|
|
|
|
<div class="flex flex-col">
|
|
|
|
|
<span class="text-monokai-comment">Ports & Services</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-6">
|
|
|
|
|
<SystemStats />
|
|
|
|
|
|
|
|
|
|
<div class="h-8 w-px bg-monokai-comment"></div>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
@click="goToDashboard"
|
|
|
|
|
class="px-4 py-2 rounded bg-monokai-yellow text-monokai-bg font-bold hover:bg-monokai-orange transition-colors"
|
|
|
|
|
title="Retour au dashboard"
|
|
|
|
|
>
|
|
|
|
|
<span class="mdi mdi-arrow-left"></span> Dashboard
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
@click="openSettings"
|
|
|
|
|
class="px-4 py-2 rounded bg-monokai-purple text-monokai-bg text-sm hover:bg-monokai-pink transition-colors"
|
|
|
|
|
title="Ouvrir les paramètres"
|
|
|
|
|
>
|
|
|
|
|
<span class="mdi mdi-cog"></span> Paramètres
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- Layout 3 colonnes -->
|
|
|
|
|
<div class="flex-1 flex overflow-hidden">
|
|
|
|
|
|
|
|
|
|
<!-- Volet gauche : Liste des services -->
|
|
|
|
|
<div class="w-80 flex-shrink-0 border-r border-monokai-comment flex flex-col">
|
|
|
|
|
<div class="p-4 border-b border-monokai-comment">
|
|
|
|
|
<h2 class="text-lg font-bold text-monokai-cyan flex items-center gap-2">
|
|
|
|
|
<span class="mdi mdi-server-network"></span>
|
|
|
|
|
Services
|
|
|
|
|
<span class="text-xs text-monokai-comment font-normal">({{ selectedCount }}/{{ services.length }})</span>
|
|
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Liste scrollable des services -->
|
|
|
|
|
<div class="flex-1 overflow-auto p-2">
|
|
|
|
|
<div
|
|
|
|
|
v-for="service in services"
|
|
|
|
|
:key="service.name + service.port"
|
|
|
|
|
class="flex items-center gap-2 px-3 py-1.5 rounded hover:bg-monokai-comment/20 cursor-pointer transition-colors"
|
|
|
|
|
@click="toggleService(service)"
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
:checked="isSelected(service)"
|
|
|
|
|
class="form-checkbox accent-monokai-cyan"
|
|
|
|
|
@click.stop
|
|
|
|
|
@change="toggleService(service)"
|
|
|
|
|
/>
|
|
|
|
|
<span class="flex-1 text-sm text-monokai-text truncate">{{ service.name }}</span>
|
|
|
|
|
<span class="text-xs px-1.5 py-0.5 rounded bg-monokai-comment/30 text-monokai-comment font-mono">
|
|
|
|
|
{{ service.port }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-if="service.protocol" class="text-xs text-monokai-purple">
|
|
|
|
|
{{ service.protocol }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="services.length === 0" class="text-center text-monokai-comment text-sm p-4">
|
|
|
|
|
Aucun service configuré dans config.yaml
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Boutons de sélection -->
|
|
|
|
|
<div class="p-3 border-t border-monokai-comment flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
@click="selectAll"
|
|
|
|
|
class="flex-1 px-2 py-1.5 rounded text-xs font-bold bg-monokai-comment/30 text-monokai-text hover:bg-monokai-comment/50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Tout cocher
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
@click="selectNone"
|
|
|
|
|
class="flex-1 px-2 py-1.5 rounded text-xs font-bold bg-monokai-comment/30 text-monokai-text hover:bg-monokai-comment/50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Tout décocher
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Zone centrale : Scan et résultats -->
|
|
|
|
|
<div class="flex-1 flex flex-col overflow-hidden">
|
|
|
|
|
|
|
|
|
|
<!-- Barre d'action -->
|
|
|
|
|
<div class="p-4 border-b border-monokai-comment">
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
<button
|
|
|
|
|
@click="launchScan"
|
|
|
|
|
:disabled="selectedCount === 0 || isScanning"
|
|
|
|
|
class="px-4 py-2 rounded font-bold transition-colors"
|
|
|
|
|
:class="selectedCount === 0 || isScanning
|
|
|
|
|
? 'bg-monokai-comment/30 text-monokai-comment cursor-not-allowed'
|
|
|
|
|
: 'bg-monokai-cyan text-monokai-bg hover:bg-monokai-green'"
|
|
|
|
|
>
|
|
|
|
|
<span class="mdi mdi-magnify-scan"></span>
|
|
|
|
|
{{ isScanning ? 'Scan en cours...' : 'Lancer le scan' }}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Progression -->
|
|
|
|
|
<div v-if="isScanning" class="flex items-center gap-3 flex-1">
|
|
|
|
|
<div class="flex-1 h-2 bg-monokai-comment/30 rounded-full overflow-hidden">
|
|
|
|
|
<div
|
|
|
|
|
class="h-full bg-monokai-cyan transition-all duration-300 rounded-full"
|
|
|
|
|
:style="{ width: progressPercent + '%' }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="text-xs text-monokai-cyan whitespace-nowrap">
|
|
|
|
|
{{ scanProgress.current }} / {{ scanProgress.total }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-if="scanProgress.ip" class="text-xs text-monokai-comment">
|
|
|
|
|
{{ scanProgress.ip }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Stats résultat -->
|
|
|
|
|
<div v-if="!isScanning && results.length > 0" class="text-sm text-monokai-comment">
|
|
|
|
|
{{ filteredResults.length }} service(s) détecté(s)
|
|
|
|
|
<span v-if="filterText" class="text-monokai-purple">(filtré)</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Message informatif sous le scan -->
|
|
|
|
|
<div v-if="isScanning" class="mt-2 text-xs text-monokai-comment flex items-center gap-2">
|
|
|
|
|
<span class="mdi mdi-information-outline text-monokai-cyan"></span>
|
|
|
|
|
Scan de {{ selectedCount }} service(s) sur l'ensemble des IPs connues du réseau.
|
|
|
|
|
Chaque IP est testée avec un timeout de {{ scanTimeout }}s.
|
|
|
|
|
<span v-if="scanProgress.total > 0" class="text-monokai-yellow">
|
|
|
|
|
Temps estimé : ~{{ estimatedTime }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Tableau de résultats -->
|
|
|
|
|
<div v-if="results.length > 0 || isScanning" class="flex-1 overflow-auto">
|
|
|
|
|
<!-- Filtre texte -->
|
|
|
|
|
<div class="px-4 py-2 sticky top-0 bg-monokai-bg border-b border-monokai-comment/50 z-10">
|
|
|
|
|
<input
|
|
|
|
|
v-model="filterText"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Filtrer les résultats (IP, nom, service...)"
|
|
|
|
|
class="w-full px-3 py-1.5 rounded bg-monokai-comment/20 text-monokai-text text-sm border border-monokai-comment/30 focus:border-monokai-cyan focus:outline-none"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<table class="w-full text-sm">
|
|
|
|
|
<thead class="sticky top-[41px] bg-monokai-bg z-10">
|
|
|
|
|
<tr class="text-monokai-comment border-b border-monokai-comment">
|
|
|
|
|
<th class="text-left px-4 py-2 cursor-pointer hover:text-monokai-text" @click="sortBy('ip')">
|
|
|
|
|
IP {{ sortIcon('ip') }}
|
|
|
|
|
</th>
|
|
|
|
|
<th class="text-left px-4 py-2 cursor-pointer hover:text-monokai-text" @click="sortBy('name')">
|
|
|
|
|
Nom {{ sortIcon('name') }}
|
|
|
|
|
</th>
|
|
|
|
|
<th class="text-left px-4 py-2 cursor-pointer hover:text-monokai-text" @click="sortBy('service_name')">
|
|
|
|
|
Service {{ sortIcon('service_name') }}
|
|
|
|
|
</th>
|
|
|
|
|
<th class="text-left px-4 py-2 cursor-pointer hover:text-monokai-text" @click="sortBy('port')">
|
|
|
|
|
Port {{ sortIcon('port') }}
|
|
|
|
|
</th>
|
|
|
|
|
<th class="text-left px-4 py-2">Lien</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr
|
|
|
|
|
v-for="(row, index) in filteredResults"
|
|
|
|
|
:key="row.ip + '-' + row.port"
|
|
|
|
|
class="border-b border-monokai-comment/20 hover:bg-monokai-comment/10 transition-colors"
|
|
|
|
|
:class="index % 2 === 0 ? 'bg-monokai-bg' : 'bg-monokai-comment/5'"
|
|
|
|
|
>
|
|
|
|
|
<td class="px-4 py-2 font-mono text-monokai-cyan">{{ row.ip }}</td>
|
|
|
|
|
<td class="px-4 py-2 text-monokai-text">{{ row.name || row.hostname || '-' }}</td>
|
|
|
|
|
<td class="px-4 py-2">
|
|
|
|
|
<span class="px-2 py-0.5 rounded text-xs font-bold"
|
|
|
|
|
:class="serviceColor(row.protocol)">
|
|
|
|
|
{{ row.service_name }}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-4 py-2 font-mono text-monokai-purple">{{ row.port }}</td>
|
|
|
|
|
<td class="px-4 py-2">
|
|
|
|
|
<a
|
|
|
|
|
v-if="row.url"
|
|
|
|
|
:href="row.url"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
class="text-monokai-green hover:text-monokai-yellow underline flex items-center gap-1"
|
|
|
|
|
>
|
|
|
|
|
<span class="mdi mdi-open-in-new text-xs"></span>
|
|
|
|
|
{{ row.url }}
|
|
|
|
|
</a>
|
|
|
|
|
<span v-else-if="row.protocol" class="text-monokai-comment text-xs">
|
|
|
|
|
{{ row.protocol }}://{{ row.ip }}:{{ row.port }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else class="text-monokai-comment text-xs">{{ row.ip }}:{{ row.port }}</span>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<!-- Message pendant le scan si pas encore de résultats -->
|
|
|
|
|
<tr v-if="filteredResults.length === 0 && isScanning">
|
|
|
|
|
<td colspan="5" class="px-4 py-8 text-center text-monokai-comment">
|
|
|
|
|
<span class="mdi mdi-magnify-scan text-2xl block mb-2 animate-pulse"></span>
|
|
|
|
|
Scan en cours, en attente de résultats...
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- État vide -->
|
|
|
|
|
<div v-else-if="!isScanning" class="flex-1 flex items-center justify-center">
|
|
|
|
|
<div class="text-center text-monokai-comment">
|
|
|
|
|
<span class="mdi mdi-lan-check text-6xl block mb-4 opacity-30"></span>
|
|
|
|
|
<div class="text-lg">Sélectionnez des services et lancez un scan</div>
|
|
|
|
|
<div class="text-sm mt-2 opacity-60">
|
|
|
|
|
Les résultats afficheront les services actifs sur chaque IP du réseau
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Volet droit : Logs -->
|
|
|
|
|
<div
|
|
|
|
|
class="relative flex-shrink-0 border-l border-monokai-comment"
|
|
|
|
|
:style="{ width: `${rightPanelWidth}px` }"
|
|
|
|
|
>
|
|
|
|
|
<!-- Poignée de redimensionnement -->
|
|
|
|
|
<div
|
|
|
|
|
class="absolute left-0 top-0 bottom-0 w-2 cursor-ew-resize"
|
|
|
|
|
@mousedown.prevent="startResize"
|
|
|
|
|
></div>
|
|
|
|
|
<div class="absolute left-1 top-1/2 -translate-y-1/2 flex flex-col gap-1 opacity-70 pointer-events-none">
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-monokai-comment/70"></span>
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-monokai-comment/70"></span>
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-monokai-comment/70"></span>
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-monokai-comment/70"></span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="p-4 h-full flex flex-col">
|
|
|
|
|
<h2 class="text-lg font-bold text-monokai-pink flex items-center gap-2">
|
|
|
|
|
<span class="mdi mdi-text-box-outline"></span>
|
|
|
|
|
Logs du scan
|
|
|
|
|
</h2>
|
|
|
|
|
<div class="mt-3 bg-monokai-bg border border-monokai-comment rounded flex-1 min-h-0 overflow-auto">
|
|
|
|
|
<pre class="text-xs text-monokai-text font-mono p-3 whitespace-pre-wrap">{{ logs.length ? logs.join('\n') : 'Aucun log pour le moment.' }}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
v-if="logs.length > 0"
|
|
|
|
|
@click="clearLogs"
|
|
|
|
|
class="mt-2 px-3 py-1 rounded text-xs bg-monokai-comment/30 text-monokai-comment hover:bg-monokai-comment/50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Effacer les logs
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Modal Paramètres -->
|
|
|
|
|
<SettingsModal
|
|
|
|
|
:isOpen="showSettings"
|
|
|
|
|
@close="showSettings = false"
|
|
|
|
|
@configReloaded="handleConfigReloaded"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
import { useIPStore } from '@/stores/ipStore'
|
|
|
|
|
import SystemStats from '@/components/SystemStats.vue'
|
|
|
|
|
import SettingsModal from '@/components/SettingsModal.vue'
|
|
|
|
|
import axios from 'axios'
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const ipStore = useIPStore()
|
|
|
|
|
const showSettings = ref(false)
|
|
|
|
|
|
|
|
|
|
// --- Données ---
|
|
|
|
|
const services = ref([])
|
|
|
|
|
const selectedPorts = ref(new Set())
|
|
|
|
|
const results = ref([])
|
|
|
|
|
const isScanning = ref(false)
|
|
|
|
|
const scanProgress = ref({ current: 0, total: 0, ip: '' })
|
|
|
|
|
const logs = ref([])
|
|
|
|
|
const filterText = ref('')
|
|
|
|
|
const sortColumn = ref('ip')
|
|
|
|
|
const sortAsc = ref(true)
|
|
|
|
|
|
|
|
|
|
// --- Panneau droit redimensionnable ---
|
|
|
|
|
const rightPanelWidth = ref(384)
|
|
|
|
|
const minPanelWidth = 288
|
|
|
|
|
const maxPanelWidth = 576
|
|
|
|
|
let isResizing = false
|
|
|
|
|
let startX = 0
|
|
|
|
|
let startWidth = 0
|
|
|
|
|
|
|
|
|
|
// --- Computed ---
|
|
|
|
|
const selectedCount = computed(() => selectedPorts.value.size)
|
|
|
|
|
|
|
|
|
|
const progressPercent = computed(() => {
|
|
|
|
|
if (scanProgress.value.total === 0) return 0
|
|
|
|
|
return Math.round((scanProgress.value.current / scanProgress.value.total) * 100)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const scanTimeout = ref(1.0)
|
|
|
|
|
|
|
|
|
|
const estimatedTime = computed(() => {
|
|
|
|
|
const total = scanProgress.value.total
|
|
|
|
|
const remaining = total - scanProgress.value.current
|
|
|
|
|
const seconds = Math.ceil(remaining * scanTimeout.value)
|
|
|
|
|
if (seconds < 60) return `${seconds}s`
|
|
|
|
|
const min = Math.floor(seconds / 60)
|
|
|
|
|
const sec = seconds % 60
|
|
|
|
|
return sec > 0 ? `${min}min ${sec}s` : `${min}min`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const filteredResults = computed(() => {
|
|
|
|
|
let data = [...results.value]
|
|
|
|
|
|
|
|
|
|
// Filtre texte
|
|
|
|
|
if (filterText.value) {
|
|
|
|
|
const search = filterText.value.toLowerCase()
|
|
|
|
|
data = data.filter(r =>
|
|
|
|
|
r.ip.includes(search) ||
|
|
|
|
|
(r.name || '').toLowerCase().includes(search) ||
|
|
|
|
|
(r.hostname || '').toLowerCase().includes(search) ||
|
|
|
|
|
r.service_name.toLowerCase().includes(search) ||
|
|
|
|
|
String(r.port).includes(search) ||
|
|
|
|
|
(r.protocol || '').toLowerCase().includes(search)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tri
|
|
|
|
|
data.sort((a, b) => {
|
|
|
|
|
let va = a[sortColumn.value]
|
|
|
|
|
let vb = b[sortColumn.value]
|
|
|
|
|
|
|
|
|
|
// Tri numérique pour IP
|
|
|
|
|
if (sortColumn.value === 'ip') {
|
|
|
|
|
const toNum = ip => ip.split('.').reduce((acc, oct) => acc * 256 + parseInt(oct), 0)
|
|
|
|
|
va = toNum(va)
|
|
|
|
|
vb = toNum(vb)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tri numérique pour port
|
|
|
|
|
if (sortColumn.value === 'port') {
|
|
|
|
|
va = Number(va)
|
|
|
|
|
vb = Number(vb)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (va < vb) return sortAsc.value ? -1 : 1
|
|
|
|
|
if (va > vb) return sortAsc.value ? 1 : -1
|
|
|
|
|
|
|
|
|
|
// Tri secondaire : par port si même IP, par IP si même colonne
|
|
|
|
|
if (sortColumn.value === 'ip') return a.port - b.port
|
|
|
|
|
const toNum = ip => ip.split('.').reduce((acc, oct) => acc * 256 + parseInt(oct), 0)
|
|
|
|
|
return toNum(a.ip) - toNum(b.ip)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// --- Méthodes ---
|
|
|
|
|
function isSelected(service) {
|
|
|
|
|
return selectedPorts.value.has(service.port)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleService(service) {
|
|
|
|
|
const ports = new Set(selectedPorts.value)
|
|
|
|
|
if (ports.has(service.port)) {
|
|
|
|
|
ports.delete(service.port)
|
|
|
|
|
} else {
|
|
|
|
|
ports.add(service.port)
|
|
|
|
|
}
|
|
|
|
|
selectedPorts.value = ports
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectAll() {
|
|
|
|
|
selectedPorts.value = new Set(services.value.map(s => s.port))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectNone() {
|
|
|
|
|
selectedPorts.value = new Set()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sortBy(column) {
|
|
|
|
|
if (sortColumn.value === column) {
|
|
|
|
|
sortAsc.value = !sortAsc.value
|
|
|
|
|
} else {
|
|
|
|
|
sortColumn.value = column
|
|
|
|
|
sortAsc.value = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sortIcon(column) {
|
|
|
|
|
if (sortColumn.value !== column) return ''
|
|
|
|
|
return sortAsc.value ? '▲' : '▼'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function serviceColor(protocol) {
|
|
|
|
|
if (!protocol) return 'bg-monokai-comment/30 text-monokai-comment'
|
|
|
|
|
switch (protocol) {
|
|
|
|
|
case 'http':
|
|
|
|
|
case 'https':
|
|
|
|
|
return 'bg-monokai-green/20 text-monokai-green'
|
|
|
|
|
case 'ssh':
|
|
|
|
|
return 'bg-monokai-cyan/20 text-monokai-cyan'
|
|
|
|
|
case 'rdp':
|
|
|
|
|
return 'bg-monokai-orange/20 text-monokai-orange'
|
|
|
|
|
case 'smb':
|
|
|
|
|
case 'ftp':
|
|
|
|
|
return 'bg-monokai-yellow/20 text-monokai-yellow'
|
|
|
|
|
default:
|
|
|
|
|
return 'bg-monokai-purple/20 text-monokai-purple'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadServices() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.get('/api/services/list')
|
|
|
|
|
services.value = response.data.services || []
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Erreur chargement services:', error)
|
|
|
|
|
addLog('Erreur chargement de la liste des services')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addLog(message) {
|
|
|
|
|
const now = new Date().toLocaleTimeString('fr-FR')
|
|
|
|
|
logs.value.push(`[${now}] ${message}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearLogs() {
|
|
|
|
|
logs.value = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function launchScan() {
|
|
|
|
|
if (selectedCount.value === 0 || isScanning.value) return
|
|
|
|
|
|
|
|
|
|
isScanning.value = true
|
|
|
|
|
results.value = []
|
|
|
|
|
scanProgress.value = { current: 0, total: 0, ip: '' }
|
|
|
|
|
|
|
|
|
|
const ports = Array.from(selectedPorts.value)
|
|
|
|
|
const serviceNames = ports.map(p => {
|
|
|
|
|
const svc = services.value.find(s => s.port === p)
|
|
|
|
|
return svc ? svc.name : `Port ${p}`
|
|
|
|
|
})
|
|
|
|
|
addLog(`Scan lancé pour ${ports.length} service(s) : ${serviceNames.join(', ')}`)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.post('/api/services/scan', { ports })
|
|
|
|
|
if (response.data.status === 'error') {
|
|
|
|
|
addLog(`Erreur : ${response.data.message}`)
|
|
|
|
|
isScanning.value = false
|
|
|
|
|
}
|
|
|
|
|
// Les résultats arrivent via WebSocket (service_scan_complete)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Erreur scan services:', error)
|
|
|
|
|
addLog(`Erreur : ${error.response?.data?.detail || error.message}`)
|
|
|
|
|
isScanning.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- WebSocket : écouter la progression du scan ---
|
|
|
|
|
function handleWsMessage(event) {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data)
|
|
|
|
|
if (data.type === 'service_scan_progress') {
|
|
|
|
|
scanProgress.value = {
|
|
|
|
|
current: data.current || 0,
|
|
|
|
|
total: data.total || 0,
|
|
|
|
|
ip: data.ip || ''
|
|
|
|
|
}
|
|
|
|
|
} else if (data.type === 'service_scan_start') {
|
|
|
|
|
addLog(data.message || 'Scan de services démarré')
|
|
|
|
|
} else if (data.type === 'service_scan_log') {
|
|
|
|
|
addLog(data.message || '')
|
|
|
|
|
} else if (data.type === 'service_scan_result') {
|
|
|
|
|
// Résultat individuel en temps réel
|
|
|
|
|
if (data.result) {
|
|
|
|
|
results.value.push(data.result)
|
|
|
|
|
}
|
|
|
|
|
} else if (data.type === 'service_scan_complete') {
|
|
|
|
|
addLog(data.message || 'Scan de services terminé')
|
|
|
|
|
// Mettre à jour avec les résultats finaux (triés)
|
|
|
|
|
if (data.results) {
|
|
|
|
|
results.value = data.results
|
|
|
|
|
}
|
|
|
|
|
isScanning.value = false
|
|
|
|
|
scanProgress.value = { current: 0, total: 0, ip: '' }
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Message non-JSON, ignorer
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Navigation ---
|
|
|
|
|
function goToDashboard() {
|
|
|
|
|
router.push('/')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openSettings() {
|
|
|
|
|
showSettings.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleConfigReloaded() {
|
|
|
|
|
await ipStore.fetchIPs()
|
|
|
|
|
ipStore.bumpConfigReload()
|
|
|
|
|
await loadServices()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Redimensionnement panneau droit ---
|
|
|
|
|
function clampWidth(value) {
|
|
|
|
|
return Math.max(minPanelWidth, Math.min(maxPanelWidth, value))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onMouseMove(event) {
|
|
|
|
|
if (!isResizing) return
|
|
|
|
|
const delta = startX - event.clientX
|
|
|
|
|
rightPanelWidth.value = clampWidth(startWidth + delta)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopResize() {
|
|
|
|
|
if (!isResizing) return
|
|
|
|
|
isResizing = false
|
|
|
|
|
window.removeEventListener('mousemove', onMouseMove)
|
|
|
|
|
window.removeEventListener('mouseup', stopResize)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startResize(event) {
|
|
|
|
|
isResizing = true
|
|
|
|
|
startX = event.clientX
|
|
|
|
|
startWidth = rightPanelWidth.value
|
|
|
|
|
window.addEventListener('mousemove', onMouseMove)
|
|
|
|
|
window.addEventListener('mouseup', stopResize)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Lifecycle ---
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await loadServices()
|
|
|
|
|
|
|
|
|
|
// Écouter les messages WebSocket pour la progression
|
|
|
|
|
if (ipStore.ws) {
|
|
|
|
|
ipStore.ws.addEventListener('message', handleWsMessage)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
stopResize()
|
|
|
|
|
if (ipStore.ws) {
|
|
|
|
|
ipStore.ws.removeEventListener('message', handleWsMessage)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
</script>
|