Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The goal of NetScan-AI is to build on top of Sonar's solid capture engine and ad
- Reconstructs packet metadata in real time and maps traffic relationships
- BPF filter builder with preset rules and live preview
- Import `.pcap` files for offline analysis
- **PCAP recording** — automatically saves a `.pcap` file to the Downloads folder whenever a capture session is started
- Supports the following protocols:

- Ethernet (MAC), VLAN (802.1Q)
Expand All @@ -57,12 +58,38 @@ The goal of NetScan-AI is to build on top of Sonar's solid capture engine and ad
- UDP, TCP
- HTTP, DNS, TLS, QUIC

### Network Graph

- **Force-directed layout** with toggleable gravity
- **Device fingerprinting** — nodes are automatically identified by MAC OUI and IP heuristics and display a matching icon:
- Router / Switch (Cisco, Ubiquiti, TP-Link, Huawei…)
- Server
- Desktop / PC (Dell, HP, Intel, Lenovo…)
- Mobile / Tablet (Samsung, Huawei, Xiaomi, Google…)
- Apple device
- Windows PC (Microsoft OUI)
- Linux / Raspberry Pi
- Printer (HP, Canon, Epson, Brother…)
- Virtual Machine (VMware, VirtualBox)
- Internet (public IPs)
- **Colored ring** around each node indicates private (blue/green) vs public (orange) address
- **Manual override** — click a node and change its device type from the dropdown in the info panel
- Node label editing with backend persistence
- **Export graph** as PNG (2× scale, white background) or SVG via the Save dropdown in the toolbar

### Export & Rules

- **CSV export** — full flow matrix as a spreadsheet
- **Snort rules** — generate `.rules` file from captured flows
- **Suricata rules** — generate `.rules` file with metadata headers
- **iptables script** — generate a bash script with ACCEPT rules for observed traffic

### UI

- Dark theme desktop app (Tauri 2 + Vue 3)
- Network graph visualisation with node inspection and label editing
- Real-time packet table
- Custom error dialogs with actionable guidance (including CAP\_NET\_RAW fix)
- Real-time packet table with scroll-to-bottom and auto-pause on manual scroll
- High-performance rendering — packets are batched via `requestAnimationFrame` to avoid UI freeze during high-traffic captures
- Custom error dialogs with actionable guidance (including `CAP_NET_RAW` fix)
- VS Code-style AI sidebar that pushes content rather than overlaying it

---
Expand Down
87 changes: 68 additions & 19 deletions src/components/AnalyseView/BottomLong.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div>
<div class="table-wrapper">
<table class="trames">
<thead>
<tr>
Expand All @@ -17,7 +17,7 @@
<th>Heure</th>
</tr>
</thead>
<tbody>
<tbody ref="tbodyEl">
<tr v-for="(packet, index) in safePackets" :key="index">
<td>{{ packet?.flow?.source_mac ?? '-' }}</td>
<td>{{ packet?.flow?.destination_mac ?? '-' }}</td>
Expand Down Expand Up @@ -48,9 +48,15 @@ export default defineComponent({
packets: [] as PacketMinimal[],
offPacket: null as null | (() => void),
resetHandler: null as null | (() => void),
MAX_ROWS: 5, // ajuste si besoin
MAX_ROWS: 500,
autoScroll: true,
}
},
watch: {
safePackets() {
this.$nextTick(() => this.scrollToBottom())
},
},
computed: {
captureStore() {
return useCaptureStore()
Expand All @@ -69,9 +75,17 @@ export default defineComponent({
// garde une taille raisonnable même en interne
if (this.packets.length > 200) this.packets.shift()
}
// Si ton store renvoie une fonction d’unsubscribe, garde-la
const maybeOff = this.captureStore.onPacket(onPacket)
if (typeof maybeOff === 'function') this.offPacket = maybeOff
if (typeof maybeOff === ‘function’) this.offPacket = maybeOff
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This line uses typographic quotes ( and ) instead of standard single quotes ('). This will cause a JavaScript syntax error and prevent the component from working correctly. The same issue is present on line 84.

    if (typeof maybeOff === 'function') this.offPacket = maybeOff


// Pause auto-scroll si l’utilisateur remonte
const tbody = this.$refs.tbodyEl as HTMLElement | undefined
if (tbody) {
tbody.addEventListener(‘scroll’, () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This line uses a typographic quote () instead of a standard single quote ('). This will cause a JavaScript syntax error.

      tbody.addEventListener('scroll', () => {

const atBottom = tbody.scrollHeight - tbody.scrollTop - tbody.clientHeight < 32
this.autoScroll = atBottom
})
}
Comment on lines +82 to +88
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

An event listener is added to the tbody element here, but it's never removed. This will cause a memory leak because the listener holds a reference to the component instance, preventing it from being garbage collected when it's unmounted. You should store the listener function in data and remove it in the beforeUnmount lifecycle hook, similar to how it's done for export handlers in NetworkGraphComponent.vue.


// Handler reset conservé pour off() symétrique si nécessaire
const reset = () => { this.packets = [] }
Expand All @@ -90,6 +104,11 @@ export default defineComponent({
}
},
methods: {
scrollToBottom() {
if (!this.autoScroll) return
const tbody = this.$refs.tbodyEl as HTMLElement | undefined
if (tbody) tbody.scrollTop = tbody.scrollHeight
},
formatTimestamp(sec?: number, usec?: number): string {
if (typeof sec !== 'number' || typeof usec !== 'number') return '-'
const date = new Date(sec * 1000 + Math.floor(usec / 1000))
Expand All @@ -112,19 +131,48 @@ export default defineComponent({
</script>

<style scoped>
.trames {
display: block;
.table-wrapper {
height: 190px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background-color: #2c2c3a;
font-family: 'Courier New', Courier, monospace;
border-top: 1px solid #383850;
overflow: hidden;
}
table {

.trames {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
font-family: 'Courier New', Courier, monospace;
}

thead {
display: table;
width: 100%;
table-layout: fixed;
flex-shrink: 0;
}

tbody {
display: block;
overflow-y: auto;
flex: 1;
min-height: 0;
}

tbody tr {
display: table;
width: 100%;
table-layout: fixed;
animation: row-in 0.18s cubic-bezier(0.25, 1, 0.5, 1);
}

td, th {
padding: 6px 8px;
text-align: center;
Expand All @@ -135,24 +183,25 @@ td, th {
text-overflow: ellipsis;
font-size: 11px;
}

th {
color: #687888;
font-weight: 500;
}
tbody {
display: block;
overflow-y: auto;

tbody::-webkit-scrollbar {
width: 4px;
}
tbody tr {
animation: row-in 0.18s cubic-bezier(0.25, 1, 0.5, 1);
tbody::-webkit-scrollbar-track {
background: #23232f;
}
tbody::-webkit-scrollbar-thumb {
background: #3c3c50;
border-radius: 2px;
}

@keyframes row-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
thead, tbody tr {
display: table;
width: 100%;
table-layout: fixed;
}
</style>
Loading
Loading