Init repo

This commit is contained in:
Wojciech Pakulski 2021-01-31 13:53:41 +01:00
commit d3199ac829
59 changed files with 28931 additions and 0 deletions

318
README.md Normal file
View File

@ -0,0 +1,318 @@
# Projekt magisterski - skaner ruchu bezprzewodowego sieci 802.11
# Autor - Wojciech Pakulski - s426211
# Podprojekty - netvu i Netview
1. netvu - skaner sieciowy, analizator plików PCAP, serwer HTTP
2. Netview - nakładka graficzna umożliwiająca interakcję z modułem serwera HTTP netvu
## Dokumentacja - netvu
### Opis
Program netvu jest aplikacją konsolową napisaną pod system Linux. Aplikacja wejściowa służy jako
pośrednik, który przekazuje odpowiednie parametry do dostępnych modułów. Przed procesem budowania
programu użytkownik może wybrać, które z modułów będą dostępne. Proces budowania i wymagane
zależności są zapisane w odpowiednich plikach `CMakeLists.txt`.
Po uruchomieniu programu bez podania parametrów bądź z flagą `-h`/`--help` ukazane zostaną
dostępne opcje i domyślne argumenty:
```
Allowed options:
-h [ --help ] print usage information and exit
-c [ --config ] arg (=netvu.conf) alternative config file
Configuration:
-m [ --mode ] arg runtime mode - scanner/analyzer/server
Scanner:
-i [ --scanner.interface ] arg wireless interface
-C [ --scanner.channels ] arg space separated 802.11 channels numbers
-M [ --scanner.max_packets ] arg (=0) packet capture limit
-l [ --scanner.log_dir ] arg (=log) where to save captured packets
-I [ --scanner.channel_hop_interval ] arg (=300)
channel hop interval in milliseconds
Analyzer:
-L [ --analyzer.log_dir ] arg (=log) where captured packets are stored
-D [ --analyzer.database_dir ] arg (=db)
where to save analisys result
Server:
-a [ --server.address ] arg (=0.0.0.0)
listening address
-p [ --server.port ] arg (=9000) listening port
-d [ --server.database_dir ] arg (=db)
where analisys result is stored
-o [ --server.cors ] arg (=*) access control allow origin
```
Argumenty nie muszą być podawane wyłącznie z poziomu terminala. Netvu postara się odczytać
także wartości z pliku konfiguracyjnego `netvu.conf`, jeśli taki istnieje. Użytkownik może
podać inną ścieżkę do pliku konfiguracyjnego przez parametr `-c`/`--config`.
Przykładowy plik konfiguracyjny wygląda następująco:
```
mode=scanner
[scanner]
max_packets=2000
[analyzer]
log_dir=pcaps
database_dir=/home/x/my_db
```
Parametr `-m`/`--mode` ustala tryb w jakim netvu zostanie uruchomiony. W przypadku próby
uruchomienia trybu, który nie został dołączony w procesie budowania, zostanie zwrócony błąd.
Dokładniejszy opis każdego z dostępnych parametrów zostanie opisany w sekcjach poświęconych
danemu modułowi.
### Budowanie
Do zbudowania wszystkich modułów wymagane są następujące narzędzia i biblioteki:
* Kompilator obsługujący C++17
* CMake 3.16
* libtins
* Boost >=1.7
* SQLite3
* nlohmann_json
* Biblioteka wątków (Threads)
Przykładowy proces zbudowania projektu może wyglądać następująco:
```
$ mkdir netvu/bin
$ cd netvu/bin
$ cmake ../src
$ cmake --build .
```
### Moduł skanera sieciowego
#### Opis
Moduł skanera składa się z kilku funkcji - nie jest nią jedynie sniffowanie ruchu Wi-Fi. Do jego
zadań należy również zapisywanie przechwyconych pakietów do plików i
przełączanie kanałów. Ponadto, w jego skład wchodzą również funkcje pomocnicze, są to:
1. Wyszukiwanie domyślnego interfejsu karty bezprzewodowej.
2. Sprawdzanie obecności enkapsulacji RadioTap.
3. Włączanie/wyłączanie trybu Monitor na czas działania skanera.
4. Odszukanie dostępnych kanałów na danym interfejsie.
Nie wszystkie zadania muszą być wykonywane po uruchomieniu skanera. Część zadań może być całkowicie
pominięta w zależności od parametrów podanych przez użytkownika. Większość zadań jest wykonywana
synchronicznie, wyjątkiem są sniffowanie pakietów i przełączanie kanałów, które są podzielone na
dwa osobne wątki.
Spośród pozostałych modułów jest to
jedyny, który musi zostać uruchomiony z podwyższonymi uprawnieniami np. poprzez `sudo`.
#### Parametry
* `interface` - określa, który interfejs sieciowy zostanie wykorzystany do skanowania .
* `channels` - oddzielone spacją numery kanałów po których interfejs będzie się przełączać.
W przypadku nie podania wartości lub wartości 0 netvu postara się automatycznie wykryć listę
dostępnych kanałów na danym interfejsie. Zostaną wówczas wykorzystane wszystkie wykryte kanały.
* `max_packets` - określa, po ilu pakietach skanowanie zostanie przerwane. W przypadku nie podania
wartości lub wartości 0 skanowanie będzie trwało do momentu przerwania przez użytkownika.
* `log_dir` - katalog do którego zostanie zapisany wynik skanowania w formacie pcap.
* `channel_hop_interval` - podawany w milisekundach czas po którym netvu będzie przełączać kanały.
#### Wymagania
* Bezprzewodowa karta sieciowa i sterownik z obsługą trybu Monitor
* Podwyższone uprawnienia systemowe
* Obecność następujących programów:
1. `ip` - w celu wykrycia obecności enkapsulacji RadioTap na karcie sieciowej
2. `ifconfig` - w celu włączania/wyłączania interfejsu karty
3. `iwconfig` - w celu przełączania między trybami Managed/Monitor na czas uruchomienia
programu, wyszukiwaniu interfejsów bezprzewodowych oraz przełączaniu kanałów
4. `iwlist` - w celu wykrycia dostępnych kanałów
#### Przykład
Wywołanie bez podania konkretnego interfejsu, skanowanie jedynie kanałów 1,6 i 12 z interwałem
przełączania kanału co pół sekundy i limitem 300 pakietów.
Przed wywołaniem programu na interfejsie WLAN włączony był tryb Managed.
```
% sudo ./netvu --scanner.channels 1 6 12 --scanner.max_packets 300 --scanner.channel_hop_interval 500 --mode scanner
Starting scanner
lo no wireless extensions.
enp11s0 no wireless extensions.
enp12s0 no wireless extensions.
br-06884e076801 no wireless extensions.
docker0 no wireless extensions.
Using wlp9s0
Turning wlp9s0 down
Enabling Monitor mode
Turning wlp9s0 up
Packet: 1/300
Packet: 2/300
Packet: 3/300
Packet: 4/300
Packet: 5/300
...
Packet: 298/300
Packet: 299/300
Packet: 300/300
Turning wlp9s0 down
Enabling Managed mode
Turning wlp9s0up
All done
```
#### Budowanie
Do zbudowania modułu skanera wymagane są następujące komponenty:
* Boost (thread, filesystem, system)
* libtins
* Threads
### Moduł analizatora
#### Opis
Zadaniem analizatora jest iteracja po wszystkich plikach pcap z danego katalogu utworzonych przez
skaner i wyciągnięcie z nich konkretnych danych. Wśród tych danych należą m. in. adresy MAC,
połączenia między urządzeniami i ilość przesłanych pakietów między hostami, średnia siła sygnału
czy interwały w których wykryto ruch z lub do danego urządzenia. Wszystkie dane wyciągnięte z
plików są
umieszczane w pamięciowej bazie danych SQLite, która jest zrzutowana do pliku po zakończeniu pracy
modułu.
### Przykład
Wywołanie modułu z domyślnymi parametrami:
```
% ./netvu --mode analyzer
Starting analyzer
Adding hosts..Done
Adding connections..Done
Adding host info..Done
Adding packet types..Done
Adding timestamps..Done
Adding scanning times..Done
% ls db
2021-01-31_13-30-01.sqlite
```
#### Kompilacja
Do skompilowania modułu analizatora wymagane są następujące komponenty:
* SQLite3
* libtins
* Threads
### Moduł serwera
#### Opis
Moduł ten pełni funkcję lekkiego serwera HTTP udostępniającego dane w formacie JSON z najnowszej
bazy SQLite wygenerowanej przez moduł analizatora. Serwer asynchronicznie otwiera połączenia po
otrzymaniu odpowiedniej metody HTTP. Serwer nie umożliwia modyfikacji bazy, jego zadaniem jest
jedynie pobieranie danych na żądanie i przesyłanie ich do klienta. W razie potrzeby istnieje
możliwość ustawienia innych parametrów nasłuchiwania i odczytu danych z innego katalogu.
#### Parametry
* `address` - adres, pod którym serwer będzie nasłuchiwał nadchodzących połączeń
* `port` - port, pod którym serwer będzie nasłuchiwał nadchodzących połączeń
* `database_dir` - katalog, w którym zawarte są pliki sqlite wygenerowane przez analizator
* `cors` - URI, na które serwer będzie odpowiadał
#### Budowanie
Do zbudowania modułu serwera wymagane są następujące komponenty:
* Boost (thread)
* SQLite3
* nlohmann_json
* Threads
## Dokumentacja - Netview
### Opis
Netview jest aplikacją internetową za pomocą której można interaktywnie obejrzeć wyniki
modułu analizatora netvu. Do komunikacji z netvu nie jest potrzebny serwer proxy - Netview można
uruchomić lokalnie otwierając plik `index.html` w przeglądarce. Aplikacja postara się automatycznie
załadować z serwera dane o hostach po jej uruchomieniu. Domyślnie Netview będzie próbować się
połączyć z serwerem na adresie lokalnym i porcie 9000. Adres ten można zmienić w widoku ustawień,
a po jego zmianie dane zostaną pobrane automatycznie.
Aplikacja składa się z:
1. Interaktywnego grafu w którym widoczne są wykryte urządzenia, połączenia a także natężenie ruchu
między urządzeniami. W widoku tym można przełączać wyświetlanie danych elementów
* Odległość wierzchołków od skanera jest wyliczona na podstawie średniej
arytmetycznej wykrytych sił sygnału (dBm).
* Grubość krawędzi jest zależna od ilości przesłanych pakietów w daną stronę. Można dzięki temu
łatwiej stwierdzić, czy dany host pełni funkcję punktu dostępowego.
* Szczegółowe informacje o urządzeniach w kolejnych zakładkach dostępne są po
zaznaczeniu wierzchołków.
* W celu ułatwienia analizy możliwe jest przemieszczanie wierzchołków.
![Graf sieci](./doc-img/graph.png)
2. Widok statystyk składający się z wykresów sumy wysłanych, odebranych i transmitowanych pakietów.
![Statystyki](./doc-img/stats.png)
3. Widok szczegółów urządzeń oraz wykrytych SSID wraz z powiązanymi z nimi punktami dostępowymi.
W widoku szczegółów dostępne są informacje takie jak:
1. Adres MAC
1. Liczba pakietów z/do danego hosta
3. Powiązany adres BSSID i SSID
4. Partnerzy z którymi host nawiązał połączenie
5. Wykres dostępności urządzenia - punkty w czasie, w których wykryto ruch z/do urządzenia
![Info](./doc-img/host_info.png)
![Punkty dostępu](./doc-img/aps.png)
4. Widoku informacji ogólnych, w którym zawarty jest wykres liczby pakietów danego typu, a także
przedziały czasowe w którym rozpoczęto i zakończono skanowanie
![Ogólne](./doc-img/overall.png)
Jeśli istnieje potrzeba zmiany adresu serwera, można to zrobić klikając w przycisk ustawień
dostępnym na pasku nawigacji. Po jego kliknięciu otworzy się okienko zmiany adresu.
![Zmiana adresu](./doc-img/disconnected.png)
### Wymagania
* Do pobrania danych konieczny jest uruchomiony moduł serwera netvu.
* Przeglądarka internetowa.
### Budowanie - wymagania
W celu zbudowania Netview z kodu źródłowego konieczna jest instalacja Vue CLI (Vue.js 3).
Do poprawnego zbudowania należy także pobrać zależności projektu zawarte w pliku `package.json`.
Następnie, należy wykonać polecenie `npm run build`. Zostanie wówczas wygenerowany katalog dist
zawierający gotową aplikację.

BIN
doc-img/aps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
doc-img/disconnected.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
doc-img/graph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

BIN
doc-img/host_info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
doc-img/overall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
doc-img/stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

3
netview/.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

7
netview/.editorconfig Normal file
View File

@ -0,0 +1,7 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100

29
netview/.eslintrc.js Normal file
View File

@ -0,0 +1,29 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/vue3-essential',
'@vue/airbnb',
'@vue/typescript/recommended',
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
],
env: {
jest: true,
},
},
],
};

26
netview/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
.DS_Store
node_modules
/dist
/tests/e2e/videos/
/tests/e2e/screenshots/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

34
netview/README.md Normal file
View File

@ -0,0 +1,34 @@
# netview
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your unit tests
```
npm run test:unit
```
### Run your end-to-end tests
```
npm run test:e2e
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
netview/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
};

3
netview/cypress.json Normal file
View File

@ -0,0 +1,3 @@
{
"pluginsFile": "tests/e2e/plugins/index.js"
}

6
netview/jest.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
transform: {
'^.+\\.vue$': 'vue-jest',
},
};

17588
netview/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
netview/package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "netview",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@popperjs/core": "^2.6.0",
"axios": "^0.21.1",
"bootstrap": "^5.0.0-beta1",
"bootstrap-icons": "^1.3.0",
"chart.js": "^2.9.4",
"core-js": "^3.6.5",
"cytoscape": "^3.17.1",
"vue": "^3.0.0",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@types/bootstrap": "^5.0.4",
"@types/chart.js": "^2.9.29",
"@types/cytoscape": "^3.14.11",
"@types/jest": "^24.0.19",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-e2e-cypress": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-airbnb": "^5.0.2",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "^2.0.0-0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-unused-imports": "^1.0.1",
"eslint-plugin-vue": "^7.0.0-0",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "~3.9.3",
"vue-jest": "^5.0.0-0"
}
}

BIN
netview/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

17
netview/public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

34
netview/src/App.vue Normal file
View File

@ -0,0 +1,34 @@
<template>
<navigation />
<router-view v-slot="{ Component }">
<keep-alive include="Network">
<component :is="Component"></component>
</keep-alive>
</router-view>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import store from './store';
import Navigation from './views/Navigation.vue';
export default defineComponent({
name: 'App',
components: {
Navigation,
},
async mounted() {
try {
await store.dispatch('loadHosts');
} catch (err) {
console.error(err);
}
},
});
</script>
<style lang="scss">
@import "~bootstrap/scss/bootstrap";
@import "~bootstrap-icons/font/bootstrap-icons.css";
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

BIN
netview/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

View File

@ -0,0 +1,17 @@
<template>
<div class="alert" :class="`alert-${type}`" role="alert">
{{ msg }}
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Alert',
props: {
type: String,
msg: String,
},
});
</script>

View File

@ -0,0 +1,93 @@
<template>
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Ustawienia</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="serverUrl" class="form-label">Adres serwera</label>
<input
type="url"
class="form-control"
id="serverUrl"
placeholder="URI:PORT"
required
v-model="url"
/>
</div>
<loading-spinner v-if="loading" msg="Łączenie.." />
<alert v-if="errorMsg" :msg="errorMessage" type="danger" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
<button type="button" class="btn btn-primary" @click="setServerURL">Zapisz</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import store from '@/store';
import axios from 'axios';
import { defineComponent } from 'vue';
import { Modal } from 'bootstrap';
import LoadingSpinner from './LoadingSpinner.vue';
import Alert from './Alert.vue';
export default defineComponent({
name: 'ConnectionFormModal',
data() {
return {
url: store.state.serverURL,
loading: false,
errorMsg: '',
};
},
components: {
LoadingSpinner,
Alert,
},
computed: {
errorMessage: {
get(): string {
return `Błąd: ${this.errorMsg}`;
},
set(newError: Error) {
this.errorMsg = newError.message;
},
},
},
methods: {
async setServerURL() {
this.loading = true;
this.errorMsg = '';
try {
const res = await axios.get(`${this.url}/hosts`);
// if (res.status) {
store.dispatch('loadHosts');
// store.commit('setServerURL', this.url);
// store.commit('setHosts', res.data);
const modal = Modal.getInstance(document.querySelector('#exampleModal') as Element);
modal.toggle();
// }
} catch (err) {
this.errorMsg = err.message;
} finally {
this.loading = false;
}
},
},
});
</script>

View File

@ -0,0 +1,60 @@
<template>
<h4 class="d-flex flex-column align-items-center">AP powiązane z danym SSID:</h4>
<div class="accordion" id="accordionExample">
<div class="accordion-item" v-for="ssid of ssids" :key="ssid">
<h2 class="accordion-header">
<button class="accordion-button" type="button" @click="collapse(ssid)">
{{ ssid }}
</button>
</h2>
<div
:id="'collapse-' + ssid"
class="accordion-collapse collapse"
aria-labelledby="headingOne"
data-bs-parent="#accordionExample"
>
<div class="accordion-body">
<ul>
<li v-for="host of hostsInNetwork(ssid)" :key="host.id">
ID: {{ host.id }}, MAC: {{ host.hw }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import store from '@/store';
import { defineComponent } from 'vue';
import { Collapse } from 'bootstrap';
export default defineComponent({
name: 'HostsInNetwork',
computed: {
ssids() {
const ssidNames = Array.from(
new Set(store.state.hosts.map((host) => host.info.ssid)),
);
return ssidNames.sort();
},
},
methods: {
hostsInNetwork(name: string) {
return store.state.hosts.filter((host) => host.info.ssid === name);
},
collapse(name: string) {
const collapseElementList = [].slice.call(
document.querySelectorAll('.collapse'),
) as HTMLElement[];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _ = collapseElementList
.filter((val) => val.id === `collapse-${name}`)
.map((collapseEl) => new Collapse(collapseEl));
},
},
});
</script>

View File

@ -0,0 +1,19 @@
<template>
<div class="d-flex align-items-center">
<strong>{{msg}}</strong>
<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Options, Vue } from 'vue-class-component';
export default defineComponent({
name: 'LoadingSpinner',
props: {
msg: String,
},
});
</script>

6
netview/src/main.ts Normal file
View File

@ -0,0 +1,6 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
createApp(App).use(store).use(router).mount('#app');

View File

@ -0,0 +1,33 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import Network from '../views/Network.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Graph',
component: Network,
},
{
path: '/hosts',
name: 'Hosts',
component: () => import('../views/Hosts.vue'),
},
{
path: '/overall',
name: 'Overall',
component: () => import('../views/Overall.vue'),
},
{
path: '/statistics',
name: 'Statistics',
component: () => import('../views/Statistics.vue'),
},
];
const router = createRouter({
// history: createWebHistory(process.env.BASE_URL),
history: createWebHashHistory(),
routes,
});
export default router;

6
netview/src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

120
netview/src/store/index.ts Normal file
View File

@ -0,0 +1,120 @@
import axios from 'axios';
import { createStore } from 'vuex';
export interface Connection {
id: number;
src: number;
dst: number;
packet_count: number;
}
export interface Info {
id: number;
host: number;
distance: number;
packets_send: number;
packets_recv: number;
packets_trans: number;
ssid: string;
bssid: string;
avg_signal: number;
}
export interface PacketType {
id: number;
name: string;
packets: number;
}
export interface Availability {
id: number;
host: number;
date: number;
}
export interface ScanDates {
id: number;
start: number;
end: number;
}
export interface Host {
id: number;
hw: string;
partners: Connection[];
info: Info;
availabilities: Availability[];
}
export default createStore({
state: {
serverURL: 'http://localhost:9000',
hosts: [] as Host[],
selectedHosts: [] as Host[],
packetTypes: [] as PacketType[],
scanDates: [] as ScanDates[],
connections: [] as Connection[],
},
mutations: {
setServerURL(state, n) {
state.serverURL = n;
},
setHosts(state, hosts) {
state.hosts = hosts;
},
setSelectedHosts(state, selectedHosts) {
state.selectedHosts = selectedHosts;
},
setPacketTypes(state, packetTypes) {
state.packetTypes = packetTypes;
},
setScanDates(state, scanDates) {
state.scanDates = scanDates;
},
},
actions: {
async loadHosts(context) {
let hosts = ((await axios.get(`${this.state.serverURL}/hosts`)).data as Host[]);
const connections = (await axios.get(`${this.state.serverURL}/connections`)).data as Connection[];
const info = (await axios.get(`${this.state.serverURL}/info`)).data as Info[];
const avail = (await axios.get(`${this.state.serverURL}/availability`)).data as Availability[];
hosts = hosts.map((host) => {
Object.assign(host, {
partners: [],
info: {},
availabilities: [],
});
const hostConnections = connections
.filter((con) => con.src === host.id);
Object.assign(host.partners, hostConnections);
const hostInfo = info.find((_info) => _info.host === host.id);
Object.assign(host.info, hostInfo);
const hostAvailabilities = avail.filter((_avail) => _avail.host === host.id);
Object.assign(host.availabilities, hostAvailabilities);
return host;
});
context.commit('setHosts', hosts);
},
async loadPacketTypes(context) {
const packetTypes = ((await axios.get(`${this.state.serverURL}/packet-types`)).data as PacketType[]);
context.commit('setPacketTypes', packetTypes);
},
async loadScanDates(context) {
const scanDates = ((await axios.get(`${this.state.serverURL}/scan-dates`)).data as ScanDates[]);
context.commit('setScanDates', scanDates);
},
},
modules: {
},
});

163
netview/src/views/Hosts.vue Normal file
View File

@ -0,0 +1,163 @@
<template>
<div class="d-flex flex-column align-items-center m-3 p-2">
<h1 class="display-5">Hosty/AP</h1>
</div>
<div class="container-fluid row border p-4" v-if="loadedHosts.length !== 0">
<h4 class="d-flex flex-column align-items-center">Informacje o hostach:</h4>
<div class="col-4">
<div class="list-group" id="list-tab" role="tablist">
<button
class="list-group-item list-group-item-action"
v-for="host of loadedHosts"
:key="host.id"
data-bs-toggle="list"
:href="'#list-' + host.id"
@click="makeGraph(host.id)"
>
<i class="bi bi-display"></i> ID: {{ host.id }}
</button>
</div>
</div>
<div class="col-8 border p-3">
<div class="tab-content" id="nav-tabContent">
<ul
class="tab-pane fade"
v-for="host of loadedHosts"
:key="host.id"
:id="'list-' + host.id"
role="tabpanel"
aria-labelledby="list-home-list"
>
<li>ID: {{ host.id }}</li>
<li>Adres MAC: {{ host.hw }}</li>
<li>Wysłane pakiety: {{ host.info.packets_send }}</li>
<li>Odebrane pakiety: {{ host.info.packets_recv }}</li>
<li>Transmitowane pakiety: {{ host.info.packets_trans }}</li>
<li>Średnia siła sygnału: {{ host.info.avg_signal }}dBm</li>
<li>BSSID: {{ host.info.bssid }}</li>
<li>SSID: {{ host.info.ssid }}</li>
<li>
<button
class="btn btn-primary"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapseExample"
aria-expanded="false"
aria-controls="collapseExample"
>
Partnerzy
</button>
</li>
<p></p>
<div class="collapse" id="collapseExample">
<div class="card card-body">
<ul v-for="partner of hostPartners(host.id)" :key="partner.id">
<li>ID: {{ partner.id }}</li>
<li>Adres MAC: {{ partner.hw }}</li>
<li>Wysłane pakiety: {{ partner.info.packets_send }}</li>
<li>Odebrane pakiety: {{ partner.info.packets_recv }}</li>
<li>Transmitowane pakiety: {{ partner.info.packets_trans }}</li>
<li>Średnia siła sygnału: {{ partner.info.avg_signal }}dBm</li>
<li>BSSID: {{ partner.info.bssid }}</li>
<li>SSID: {{ partner.info.ssid }}</li>
<p></p>
</ul>
</div>
</div>
<li v-if="this.avGraph">Dostępność:</li>
<canvas
v-show="this.avGraph"
:class="'availabilitygraph-' + host.id"
width="50"
height="20"
></canvas>
</ul>
</div>
</div>
</div>
<alert v-else msg="Nie wybrano żadnych hostów" type="warning" />
<hosts-in-network />
</template>
<script lang="ts">
import store, { Host } from '@/store';
import { defineComponent } from 'vue';
import chart from 'chart.js';
import HostsInNetwork from '../components/HostsInNetwork.vue';
import Alert from '../components/Alert.vue';
export default defineComponent({
name: 'Hosts',
components: {
HostsInNetwork,
Alert,
},
data() {
return {
avGraph: null as unknown,
};
},
computed: {
loadedHosts() {
return store.state.selectedHosts;
},
},
methods: {
makeGraph(id: number) {
const availabilitygraphs = document.getElementsByClassName(
`availabilitygraph-${id}`,
);
if (!availabilitygraphs.length) {
return;
}
const canvas = availabilitygraphs[0] as HTMLCanvasElement;
const host = this.loadedHosts.find((_host) => _host.id === id);
if (!host) {
return;
}
const data = host.availabilities.map((av) => ({
x: new Date(av.date * 1000).toLocaleString(),
y: 1,
}));
if (!data) {
return;
}
this.avGraph = new chart.Chart(
canvas.getContext('2d') as CanvasRenderingContext2D,
{
type: 'line',
data: {
labels: data.map((v) => v.x),
datasets: [
{
label: 'Wykryty pakiet z/do (interwał >= 5m)',
data,
borderColor: 'blue',
fill: false,
},
],
},
},
);
},
hostPartners(id: number): Host[] {
const host = store.state.hosts.find((h) => h.id === id) as Host;
if (host?.partners.length !== 0) {
return host.partners.map((val) =>
// eslint-disable-next-line implicit-arrow-linebreak
store.state.hosts.find((_host) => _host.id === val.dst)) as Host[];
}
return [];
},
},
});
</script>

View File

@ -0,0 +1,142 @@
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light p-3 justify-content-between">
<!-- <div class="d-flex justify-content-start container-fluid"> -->
<!-- <div> -->
<div class="m-3 gap-3 d-flex align-items-center">
<img
src="@/assets/logo.png"
alt="Netview logo"
width="100"
height="100"
/>
<h1 class="navbar-brand">Netview</h1>
</div>
<!-- </div> -->
<div>
<ul class="nav nav-pills nav-fill">
<li class="nav-item">
<router-link
class="nav-link"
:class="{ active: isGraphRoute }"
to="/"
>Graf</router-link
>
</li>
<li class="nav-item">
<router-link
class="nav-link"
:class="{ active: isStatisticsRoute }"
to="/statistics"
>Statystyki</router-link
>
</li>
<li class="nav-item">
<router-link
class="nav-link"
to="/hosts"
:class="{ active: isHostsRoute }"
>Hosty/AP</router-link
>
</li>
<li class="nav-item">
<router-link
class="nav-link"
:class="{ active: isOverallRoute }"
to="/overall"
>Informacje ogólne</router-link
>
</li>
</ul>
</div>
<!-- </div> -->
<div class="d-flex align-self-">
<div class="row">
<div class="col nav-opts" >
<button
class="btn btn-primary m-2"
type="button"
data-bs-toggle="modal"
data-bs-target="#exampleModal"
>
<i class="bi bi-gear-wide-connected"></i>
Ustawienia
</button>
<connection-form-modal />
<alert v-if="connection" msg="Status: Połączono" type="success" />
<alert v-else msg="Status: Brak połączenia" type="danger" />
<div class="card" :class="{ 'bg-info': selectedHostsCount > 0 }">
<div class="card-body">
Wybrano {{ selectedHostsCount }} hostów
<button
type="button"
class="btn-close"
aria-label="Close"
@click="clearSelectedHosts"
></button>
</div>
</div>
</div>
</div>
</div>
</nav>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Modal } from 'bootstrap';
import Alert from '@/components/Alert.vue';
import ConnectionFormModal from '../components/ConnectionFormModal.vue';
import store from '../store/index';
export default defineComponent({
name: 'Navigation',
components: {
ConnectionFormModal,
Alert,
},
computed: {
connection() {
return store.state.hosts.length > 0;
},
selectedHostsCount() {
return store.state.selectedHosts.length;
},
isGraphRoute() {
return this.$route.name === 'Graph';
},
isHostsRoute() {
return this.$route.name === 'Hosts';
},
isOverallRoute() {
return this.$route.name === 'Overall';
},
isStatisticsRoute() {
return this.$route.name === 'Statistics';
},
},
methods: {
// eslint-disable-next-line no-underscore-dangle
_placeholder() {
const modal = new Modal(
document.getElementById('connectionModal') as HTMLElement,
);
},
clearSelectedHosts() {
store.commit('setSelectedHosts', []);
},
},
});
</script>
<style lang="scss" scoped>
.nav-opts {
display: flex;
flex-direction: column;
align-items: stretch;
}
</style>

View File

@ -0,0 +1,222 @@
<template>
<div class="d-flex flex-column align-items-center m-3 p-2">
<h1 class="display-5">Graf ruchu bezprzewodowego</h1>
<div class="m-3">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="flexSwitchCheckChecked"
@change="switchNodes"
:checked="showNodes"
v-model="showNodes"
/>
<label class="form-check-label" for="flexSwitchCheckChecked"
>Hosty - wierzchołki</label
>
</div>
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="flexSwitchCheckDefault"
@change="switchEdges"
v-model="showEdges"
:checked="showEdges"
/>
<label class="form-check-label" for="flexSwitchCheckDefault"
>Połączenia - krawędzie</label
>
</div>
<form class="d-flex m-3">
<input
class="form-control me-2"
type="search"
placeholder="Znajdź ID"
aria-label="Search"
v-model="search"
@keyup="findIds"
/>
</form>
</div>
</div>
<div class="border border-info border-3">
<div id="graph"></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import cytoscape, {
Core,
ElementDefinition,
} from 'cytoscape';
import store, { Host } from '@/store';
export default defineComponent({
name: 'Network',
data() {
return {
cy: (null as unknown) as Core,
nodes: [] as ElementDefinition[],
edges: [] as ElementDefinition[],
showNodes: true,
showEdges: false,
search: '',
};
},
mounted() {
if (store.state.hosts.length !== 0) {
this.updateGraph();
}
store.subscribe((mutation) => {
if (mutation.type === 'setHosts') {
this.updateGraph();
}
});
},
// computed: {
// cons() {
// return store.state.
// }
// }
methods: {
findIds() {
this.cy.filter((sel) => +sel.data('id') === +this.search)
.forEach((ele) => { ele.addClass('found'); });
this.cy.filter((sel) => +sel.data('id') !== +this.search)
.forEach((ele) => { ele.removeClass('found'); });
},
updateGraph() {
const nodes = store.state.hosts.map((host) => ({
group: 'nodes',
classes: 'hosts',
data: {
id: `${host.id}`,
distance: host.info.avg_signal ? host.info.avg_signal : 0,
},
})) as ElementDefinition[];
const edges = store.state.hosts.flatMap((host) => host.partners.map((partner) => ({
group: 'edges',
classes: 'connections hide',
data: {
source: host.id,
target: partner.dst,
weight: Math.log10(partner.packet_count),
},
}))) as ElementDefinition[];
this.edges = edges;
this.nodes = nodes;
this.cy = cytoscape({
container: document.getElementById('graph'),
elements: [
{
group: 'nodes',
data: { id: 'S', distance: 99 },
classes: 'root',
selectable: false,
grabbable: false,
},
...nodes,
...edges,
],
style: [
{
selector: '.root',
style: {
shape: 'ellipse',
'background-color': '#0077b6',
label: 'data(id)',
},
},
{
selector: '.hosts',
style: {
shape: 'round-triangle',
label: 'data(id)',
},
},
{
selector: '.connections',
style: {
width: 'data(weight)',
'line-color': '#ccc',
'target-arrow-color': '#ccc',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
},
},
{
selector: '.hide',
style: {
display: 'none',
},
},
{
selector: '.found',
style: {
'background-color': 'green',
},
},
],
layout: {
name: 'concentric',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
concentric(node: any) {
return node.data('distance');
},
levelWidth() {
return 10;
},
spacingFactor: 1.5,
},
});
this.cy.on('select', (evt) => {
const node = evt.target;
if (node.group() === 'nodes') {
store.state.selectedHosts.push(
store.state.hosts.find((host) => host.id === +node.data().id) as Host,
);
}
});
this.cy.on('unselect', (evt) => {
const node = evt.target;
if (node.group() === 'nodes') {
store.state.selectedHosts = store.state.selectedHosts.filter(
(host) => host.id !== +node.data().id,
);
}
});
},
switchNodes() {
if (this.showNodes) {
this.cy.elements('.hosts').removeClass('hide');
} else {
this.cy.elements('.hosts').addClass('hide');
}
},
switchEdges() {
if (this.showEdges) {
this.cy.elements('.connections').removeClass('hide');
} else {
this.cy.elements('.connections').addClass('hide');
}
},
},
});
</script>
<style lang="scss" scoped>
#graph {
width: 100%;
height: 500px;
display: block;
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<alert v-if="err" :msg="err.message" type="danger" />
<div class="d-flex flex-column align-items-center m-3 p-2">
<h1 class="display-5">Informacje ogólne</h1>
</div>
<div class="container-fluid">
<canvas id="packetTypeChart"></canvas>
</div>
<div class=" d-flex flex-column align-items-center m-5" v-if="scanDates.length">
<h4>
Przedziały czasowe skanowania
</h4>
<table class="table d-flex flex-column align-items-center">
<thead>
<tr>
<th scope="col">Rozpoczęto</th>
<th scope="col">Zakończono</th>
</tr>
</thead>
<tbody>
<tr v-for="scanDate of scanDates" :key="scanDate.id">
<td>{{scanDate.start}}</td>
<td>{{scanDate.end}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import store from '@/store';
import { Chart } from 'chart.js';
import Alert from '@/components/Alert.vue';
export default defineComponent({
components: {
Alert,
},
name: 'Overall',
data() {
return {
packetTypeGraph: Chart as unknown,
err: (null as unknown) as Error,
};
},
computed: {
packetTypes() {
return store.state.packetTypes;
},
scanDates() {
return store.state.scanDates.map((scanDate) => ({
id: scanDate.id,
start: new Date(scanDate.start * 1000).toLocaleString(),
end: new Date(scanDate.end * 1000).toLocaleString(),
}));
},
},
async mounted() {
store.subscribe((mutation) => {
if (mutation.type === 'setPacketTypes') {
this.updatePacketTypeGraph();
}
});
if (this.packetTypes.length === 0) {
try {
await store.dispatch('loadPacketTypes');
} catch (err) {
this.err = err;
}
} else {
this.updatePacketTypeGraph();
}
if (this.scanDates.length === 0) {
try {
await store.dispatch('loadScanDates');
} catch (err) {
this.err = err;
}
}
},
methods: {
randomColor() {
const letters = '0123456789ABCDEF'.split('');
let color = '#';
for (let i = 0; i < 6; i += 1) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
},
updatePacketTypeGraph() {
const canvas = document.getElementById(
'packetTypeChart',
) as HTMLCanvasElement;
if (!canvas) {
return;
}
const context = canvas.getContext('2d') as CanvasRenderingContext2D;
this.packetTypeGraph = new Chart(context, {
type: 'bar',
data: {
labels: this.packetTypes.map((pt) => pt.name),
datasets: [
{
label: 'Liczba pakietów',
data: this.packetTypes.map((pt) => pt.packets),
backgroundColor: this.packetTypes.map(() => this.randomColor()),
},
],
},
options: {
title: {
display: true,
fontSize: 20,
text: 'Zanotowane rodzaje pakietów',
},
legend: {
labels: {
fontSize: 16,
},
},
},
});
},
},
});
</script>

View File

@ -0,0 +1,132 @@
<template>
<div class="d-flex flex-column align-items-center m-3 p-2">
<h1 class="display-5">Statystyki</h1>
</div>
<alert
msg="Nie wybrano żadnych hostów"
type="warning"
v-if="hosts.length === 0"
/>
<div>
<div class="container-fluid">
<canvas id="receivedChart"></canvas>
</div>
<div class="container-fluid">
<canvas id="sendChart"></canvas>
</div>
<div class="container-fluid">
<canvas id="transChart"></canvas>
</div>
</div>
</template>
<script lang="ts">
import store from '@/store';
import { defineComponent } from 'vue';
import { Chart } from 'chart.js';
import Alert from '../components/Alert.vue';
export default defineComponent({
name: 'Statistics',
components: {
Alert,
},
data() {
return {
charts: Array(3).fill(null) as Chart[],
colors: [] as string[],
};
},
computed: {
hosts() {
return store.state.selectedHosts;
},
},
mounted() {
if (this.hosts.length !== 0) {
this.colors = this.hosts.map(() => this.randomColor());
this.updateGraph(
'receivedChart',
'Liczba odebranych pakietów',
this.hosts.map((host) => host.info.packets_recv),
);
this.updateGraph(
'sendChart',
'Liczba wysłanych pakietów',
this.hosts.map((host) => host.info.packets_send),
);
this.updateGraph(
'transChart',
'Liczba transmitowanych pakietów',
this.hosts.map((host) => host.info.packets_trans),
);
}
},
methods: {
randomColor() {
const letters = '0123456789ABCDEF'.split('');
let color = '#';
for (let i = 0; i < 6; i += 1) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
},
updateGraph(elementId: string, label: string, data: number[]) {
this.charts.map((thisChart) => {
if (thisChart !== null) {
thisChart.clear();
thisChart.destroy();
}
const canvas = document.getElementById(elementId) as HTMLCanvasElement;
if (!canvas) {
return null;
}
return new Chart(canvas.getContext('2d') as CanvasRenderingContext2D, {
type: 'pie',
data: {
labels: this.hosts.map(
(host) => `(ID: ${host.id}, MAC: ${host.hw})`,
),
datasets: [
{
data,
backgroundColor: this.colors,
},
],
},
options: {
title: {
display: true,
fontSize: 20,
text: label,
},
// legend: {
// labels: {
// fontSize: 5,
// },
// },
// scales: {
// xAxes: [
// {
// fontSize: 3,
// scaleLabel: {
// fontSize: 2,
// },
// },
// ],
// },
},
});
});
},
},
});
</script>

View File

@ -0,0 +1,12 @@
module.exports = {
plugins: [
'cypress',
],
env: {
mocha: true,
'cypress/globals': true,
},
rules: {
strict: 'off',
},
};

View File

@ -0,0 +1,26 @@
/* eslint-disable arrow-body-style */
// https://docs.cypress.io/guides/guides/plugins-guide.html
// if you need a custom webpack configuration you can uncomment the following import
// and then use the `file:preprocessor` event
// as explained in the cypress docs
// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples
// /* eslint-disable import/no-extraneous-dependencies, global-require */
// const webpack = require('@cypress/webpack-preprocessor')
module.exports = (on, config) => {
// on('file:preprocessor', webpack({
// webpackOptions: require('@vue/cli-service/webpack.config'),
// watchOptions: {}
// }))
return {
...config,
fixturesFolder: 'tests/e2e/fixtures',
integrationFolder: 'tests/e2e/specs',
screenshotsFolder: 'tests/e2e/screenshots',
videosFolder: 'tests/e2e/videos',
supportFile: 'tests/e2e/support/index.js',
};
};

View File

@ -0,0 +1,8 @@
// https://docs.cypress.io/api/introduction/api.html
describe('My First Test', () => {
it('Visits the app root url', () => {
cy.visit('/');
cy.contains('h1', 'Welcome to Your Vue.js + TypeScript App');
});
});

View File

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message';
const wrapper = shallowMount(HelloWorld, {
props: { msg },
});
expect(wrapper.text()).toMatch(msg);
});
});

42
netview/tsconfig.json Normal file
View File

@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
// "noImplicitAny": false,
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

3
netview/vue.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? '' : '/',
};

View File

@ -0,0 +1,108 @@
#ifndef ANALYZER_H
#define ANALYZER_H
#include <cstdint>
#include <list>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <sqlite3.h>
#include <tins/packet.h>
namespace Analyzer {
const std::string HOSTS_TABLE = R"EOF(
CREATE TABLE hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
hw VARCHAR(255)
);
)EOF";
const std::string CONNECTIONS_TABLE = R"EOF(
CREATE TABLE connections (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
src INTEGER,
dst INTEGER,
packet_count INTEGER,
FOREIGN KEY (src) REFERENCES hosts(id),
FOREIGN KEY (dst) REFERENCES hosts(id)
);
)EOF";
const std::string INFO_TABLE = R"EOF(
CREATE TABLE info (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
host INTEGER,
packets_send INTEGER,
packets_recv INTEGER,
packets_trans INTEGER,
avg_signal INTEGER,
ssid VARCHAR(255),
bssid VARCHAR(255),
FOREIGN KEY (host) REFERENCES hosts(id)
);
)EOF";
const std::string PACKET_TYPES_TABLE = R"EOF(
CREATE TABLE packet_types (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name VARCHAR(255),
packets INTEGER
);
)EOF";
const std::string SCAN_DATES_TABLE = R"EOF(
CREATE TABLE scan_dates (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
start INTEGER,
end INTEGER
);
)EOF";
const std::string AVAILABILITY_TABLE = R"EOF(
CREATE TABLE availability (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
host INTEGER,
date INTEGER,
FOREIGN KEY (host) REFERENCES hosts(id)
);
)EOF";
std::string getNow();
class PCAPAnalyzer {
struct HostInfo {
int packetsSend = 0;
int packetsReceived = 0;
int packetsTransmitted = 0;
int distance = 0;
std::string ssid = "";
std::string bssid = "";
std::vector<int> signalStrengths = {};
};
std::vector<std::string> pcaps;
std::string dbPath;
std::set<std::string> hosts;
std::map<std::string, std::map<std::string, int>> connections;
std::map<std::string, std::list<long>> availability;
std::vector<std::pair<std::uint32_t, std::uint32_t>> scanDates;
std::map<std::string, HostInfo> hostsInfo;
std::map<std::string, int> packetTypes;
long availInterval = 300;
void setupPath();
void addHosts(sqlite3* db);
void addConnections(sqlite3* db);
void addHostInfo(sqlite3* db);
void addPacketTypes(sqlite3* db);
void addTimestamps(sqlite3* db);
void addScanTimes(sqlite3* db);
void appendAvailability(const std::string& host, long timestamp);
public:
PCAPAnalyzer(
const std::string& logDir = "log",
const std::string& dbDir = "db"
);
void start();
};
};
#endif

View File

@ -0,0 +1,49 @@
add_library(Analyzer PCAPAnalyzer.cpp)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
find_package(SQLite3 REQUIRED)
if(SQLite3_FOUND)
list(APPEND EXTRA_LIBS
SQLite::SQLite3
)
endif()
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
if(Threads_FOUND)
list(APPEND EXTRA_LIBS
Threads::Threads
)
endif()
find_package(libtins)
if(libtins_FOUND)
list(APPEND EXTRA_LIBS
tins
)
else()
find_library(TINS_LIBRARY tins REQUIRED)
if(TINS_LIBRARY)
list(APPEND EXTRA_LIBS
tins
)
endif()
endif()
target_include_directories(
Analyzer
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(netvu PUBLIC ${EXTRA_LIBS})
install(TARGETS Analyzer DESTINATION lib)
install(FILES Analyzer.h DESTINATION include)

View File

@ -0,0 +1,609 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <limits>
#include <memory>
#include <numeric>
#include <set>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include <sqlite3.h>
#include <nlohmann/json.hpp>
#include <tins/data_link_type.h>
#include <tins/exceptions.h>
#include <tins/ip.h>
#include <tins/packet.h>
#include <tins/pdu.h>
#include <tins/rawpdu.h>
#include <tins/sniffer.h>
#include <tins/tins.h>
#include "Analyzer.h"
namespace Analyzer {
namespace fs = std::filesystem;
std::map<std::string, std::string> accessPoints;
std::string getNow() {
auto now = std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now()
);
auto localNow = *std::localtime(&now);
std::ostringstream localTimeString;
localTimeString << std::put_time(&localNow, "%F %T");
auto locString = localTimeString.str();
std::replace(
locString.begin(),
locString.end(),
' ',
'_'
);
std::replace(
locString.begin(),
locString.end(),
':',
'-'
);
return locString;
}
PCAPAnalyzer::PCAPAnalyzer(
const std::string& logDir,
const std::string& dbDir
) {
auto pcapFilesPath = fs::path(logDir);
for (const auto& file : fs::directory_iterator(pcapFilesPath)) {
if (file.path().extension().string() == ".pcap") {
this->pcaps.emplace_back(file.path().string());
}
}
this->dbPath = dbDir;
if (!fs::exists(this->dbPath)) {
auto res = fs::create_directory(this->dbPath);
if (!res) {
std::cerr << "Failed to create " << this->dbPath << " - using .\n";
this->dbPath = ".";
}
}
this->dbPath += "/";
std::array packetTypes{
"Control",
"Data",
"Management",
};
for (auto type : packetTypes) {
this->packetTypes[type] = 0;
}
}
void PCAPAnalyzer::addHosts(sqlite3* db) {
char *errMsg = 0;
int res = sqlite3_exec(
db,
Analyzer::HOSTS_TABLE.data(),
nullptr,
0,
&errMsg
);
if (res != SQLITE_OK) {
throw std::runtime_error(errMsg);
}
std::string insert = "INSERT INTO hosts (hw) VALUES ";
for (auto host : this->hosts) {
insert.append("('" + host + "')").append(",");
}
insert.pop_back();
insert += ';';
res = sqlite3_exec(
db,
insert.c_str(),
nullptr,
0,
&errMsg
);
if (res != SQLITE_OK) {
std::cerr << errMsg << '\n';
}
}
void PCAPAnalyzer::addConnections(sqlite3* db) {
char *errMsg = 0;
int res = sqlite3_exec(
db,
Analyzer::CONNECTIONS_TABLE.data(),
nullptr,
0,
&errMsg
);
if (res != SQLITE_OK) {
throw std::runtime_error(errMsg);
}
for (auto con : this->connections) {
for (auto val : con.second) {
std::string insert = "INSERT INTO connections(src, dst, packet_count) ";
insert += "SELECT id, (SELECT id FROM hosts WHERE hw = '" + con.first + "'), ";
insert += std::to_string(val.second);
insert += " FROM hosts WHERE hw = '" + val.first + "';";
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
if (res != SQLITE_OK) {
std::cerr << errMsg << '\n';
}
}
}
}
void PCAPAnalyzer::addHostInfo(sqlite3* db) {
char *errMsg = 0;
int res = sqlite3_exec(
db,
Analyzer::INFO_TABLE.data(),
nullptr,
0,
&errMsg
);
if (res != SQLITE_OK) {
throw std::runtime_error(errMsg);
}
for (auto host : this->hostsInfo) {
double avgSignal = 0;
if (!host.second.signalStrengths.empty()) {
avgSignal = std::floor(std::accumulate(
host.second.signalStrengths.begin(),
host.second.signalStrengths.end(),
0
) / host.second.signalStrengths.size()) * -1;
}
std::string insert = "INSERT INTO info(host, packets_send, packets_recv, ";
insert += "packets_trans, avg_signal, ssid, bssid) SELECT id ";
if (host.second.packetsSend) {
insert += ", ";
insert += std::to_string(host.second.packetsSend);
} else {
insert += ", ";
insert += "0";
}
if (host.second.packetsReceived) {
insert += ", ";
insert += std::to_string(host.second.packetsReceived);
} else {
insert += ", ";
insert += "0";
}
if (host.second.packetsTransmitted) {
insert += ", ";
insert += std::to_string(host.second.packetsTransmitted);
} else {
insert += ", ";
insert += "0";
}
insert += ", ";
insert += std::to_string(avgSignal);
if (host.second.ssid.empty()) {
insert += ", ";
insert += "'?'";
} else {
insert += ", ";
insert += "'" + host.second.ssid + "'";
}
if (host.second.bssid.empty()) {
insert += ", ";
insert += "'?'";
} else {
insert += ", ";
insert += "'" + host.second.bssid + "'";
}
insert += " FROM hosts WHERE hw = '" + host.first + "';";
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
if (res != SQLITE_OK) {
std::cerr << errMsg << '\n';
}
}
}
void PCAPAnalyzer::addPacketTypes(sqlite3* db) {
char *errMsg = 0;
int res = sqlite3_exec(
db,
Analyzer::PACKET_TYPES_TABLE.data(),
nullptr,
0,
&errMsg
);
if (res != SQLITE_OK) {
std::cerr << errMsg << '\n';
throw std::runtime_error(errMsg);
}
std::string insert = "INSERT INTO packet_types(name, packets) VALUES ";
for (auto type : this->packetTypes) {
insert += "('" + type.first + "', " + std::to_string(type.second) + "),";
}
insert.pop_back();
insert += ';';
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
if (res != SQLITE_OK) {
std::cerr << errMsg << '\n';
}
}
void PCAPAnalyzer::addTimestamps(sqlite3* db) {
char *errMsg = 0;
int res = sqlite3_exec(
db,
Analyzer::AVAILABILITY_TABLE.data(),
nullptr,
0,
&errMsg
);
if (res != SQLITE_OK) {
std::cerr << errMsg << '\n';
throw std::runtime_error(errMsg);
}
for (auto host : this->availability) {
for (auto date : host.second) {
std::string insert = "INSERT INTO availability(host, date) VALUES (";
insert += " (SELECT id FROM hosts WHERE hw = '" + host.first + "'), ";
insert += std::to_string(date);
insert += ");";
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
if (res != SQLITE_OK) {
std::cerr << errMsg << '\n';
}
}
}
}
void PCAPAnalyzer::addScanTimes(sqlite3* db) {
char *errMsg = 0;
int res = sqlite3_exec(
db,
Analyzer::SCAN_DATES_TABLE.data(),
nullptr,
0,
&errMsg
);
if (res != SQLITE_OK) {
std::cerr << errMsg << '\n';
throw std::runtime_error(errMsg);
}
for (auto scanDate : this->scanDates) {
std::string insert = "INSERT INTO scan_dates(start, end) VALUES (";
insert += std::to_string(scanDate.first);
insert += ", ";
insert += std::to_string(scanDate.second);
insert += ");";
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
if (res != SQLITE_OK) {
std::cerr << errMsg << '\n';
}
}
}
void PCAPAnalyzer::appendAvailability(const std::string& host, long timestamp) {
if (this->availability[host].size()) {
auto last = this->availability[host].back();
if (std::abs(timestamp - last) > this->availInterval) {
this->availability[host].push_back(timestamp);
}
} else {
this->availability[host].push_back(timestamp);
}
}
void PCAPAnalyzer::start() {
if (this->pcaps.empty()) {
throw std::runtime_error("No .pcap files found!");
}
for (auto pcapFile : this->pcaps) {
Tins::FileSniffer sniffer(pcapFile);
int firstScanDate = 0;
int lastScanDate = 0;
std::vector<std::uint32_t> packetTimestamps;
try {
for (auto packet : sniffer) {
packetTimestamps.emplace_back(packet.timestamp().seconds());
auto pdu = packet.release_pdu();
auto dbm = std::numeric_limits<int>::max();
auto radioTap = false;
while (pdu) {
if (pdu->matches_flag(Tins::PDU::PDUType::RADIOTAP)) {
auto radioTapPDU = dynamic_cast<Tins::RadioTap*>(pdu);
radioTap = true;
auto dot11PDU = radioTapPDU->find_pdu<Tins::Dot11>();
if (dot11PDU) {
try {
dbm = radioTapPDU->dbm_signal();
} catch (Tins::field_not_present&) {
dbm = 100;
}
}
}
if (!radioTap) {
if (pdu->matches_flag(Tins::PDU::PDUType::DOT11)) {
auto dot11PDU = dynamic_cast<Tins::Dot11*>(pdu);
auto dst = dot11PDU->addr1().to_string();
this->hostsInfo[dst].signalStrengths.emplace_back(100);
}
}
if (pdu->matches_flag(Tins::PDU::PDUType::DOT11_MANAGEMENT)) {
this->packetTypes["Management"] += 1;
auto managementPDU = dynamic_cast<Tins::Dot11ManagementFrame*>(pdu);
auto dst = managementPDU->addr1().to_string();
auto trn = managementPDU->addr2().to_string();
auto src = managementPDU->addr3().to_string();
this->hosts.insert(src);
this->hosts.insert(trn);
this->hosts.insert(dst);
this->connections[trn][dst] += 1;
if (src != dst) {
this->connections[src][dst] += 1;
}
try {
if (this->hostsInfo[trn].ssid.empty()
&& !managementPDU->from_ds()
&& !managementPDU->to_ds()) {
this->hostsInfo[trn].ssid = managementPDU->ssid();
}
} catch (Tins::option_not_found&) {}
if (!managementPDU->addr3().is_broadcast()) {
this->hostsInfo[src].packetsSend += 1;
}
this->hostsInfo[trn].packetsTransmitted += 1;
this->hostsInfo[dst].packetsReceived += 1;
auto timestamp = packet.timestamp().seconds();
if (!managementPDU->addr3().is_broadcast()) {
appendAvailability(src, timestamp);
}
appendAvailability(trn, timestamp);
if (dbm != std::numeric_limits<int>::max()) {
if (!managementPDU->addr3().is_broadcast()) {
this->hostsInfo[src].signalStrengths.emplace_back(std::abs(dbm));
}
this->hostsInfo[trn].signalStrengths.emplace_back(std::abs(dbm));
this->hostsInfo[dst].signalStrengths.emplace_back(std::abs(dbm));
}
} else if (pdu->matches_flag(Tins::PDU::PDUType::DOT11_CONTROL)) {
this->packetTypes["Control"] += 1;
auto controlPDU = dynamic_cast<Tins::Dot11Control*>(pdu);
auto dst = controlPDU->addr1().to_string();
this->hosts.insert(dst);
this->hostsInfo[dst].packetsReceived += 1;
if (dbm != std::numeric_limits<int>::max()) {
this->hostsInfo[dst].signalStrengths.emplace_back(std::abs(dbm));
}
if (pdu->matches_flag(Tins::PDU::PDUType::DOT11_RTS)) {
auto rtsPDU = dynamic_cast<Tins::Dot11RTS*>(controlPDU);
auto trn = rtsPDU->target_addr().to_string();
if (!rtsPDU->target_addr().is_broadcast()) {
this->hosts.insert(trn);
this->hostsInfo[trn].packetsTransmitted += 1;
}
this->connections[trn][dst] += 1;
}
auto timestamp = packet.timestamp().seconds();
appendAvailability(dst, timestamp);
} else if (pdu->matches_flag(Tins::PDU::PDUType::DOT11_DATA)) {
this->packetTypes["Data"] += 1;
auto dataPDU = dynamic_cast<Tins::Dot11Data*>(pdu);
auto dst = dataPDU->dst_addr().to_string();
auto trn = dataPDU->addr2().to_string();
auto src = dataPDU->src_addr().to_string();
this->hosts.insert(dst);
this->hosts.insert(trn);
this->hosts.insert(src);
this->connections[trn][dst] += 1;
if (!dataPDU->src_addr().is_broadcast()) {
this->connections[src][trn] += 1;
}
if (!dataPDU->src_addr().is_broadcast()) {
if (this->hostsInfo[src].bssid.empty()) {
this->hostsInfo[src].bssid = dataPDU->bssid_addr().to_string();
}
}
if (!dataPDU->dst_addr().is_broadcast()) {
if (this->hostsInfo[dst].bssid.empty()) {
this->hostsInfo[dst].bssid = dataPDU->bssid_addr().to_string();
}
}
if (!dataPDU->addr2().is_broadcast()) {
if (this->hostsInfo[trn].bssid.empty()) {
this->hostsInfo[trn].bssid = dataPDU->bssid_addr().to_string();
}
}
if (!dataPDU->src_addr().is_broadcast()) {
this->hostsInfo[src].packetsSend += 1;
}
this->hostsInfo[trn].packetsTransmitted += 1;
this->hostsInfo[dst].packetsReceived += 1;
auto timestamp = packet.timestamp().seconds();
if (!dataPDU->src_addr().is_broadcast()) {
appendAvailability(src, timestamp);
}
appendAvailability(trn, timestamp);
appendAvailability(dst, timestamp);
if (dbm != std::numeric_limits<int>::max()) {
if (!dataPDU->src_addr().is_broadcast()) {
this->hostsInfo[src].signalStrengths.emplace_back(std::abs(dbm));
}
this->hostsInfo[trn].signalStrengths.emplace_back(std::abs(dbm));
this->hostsInfo[dst].signalStrengths.emplace_back(std::abs(dbm));
}
} else {
if (pdu->matches_flag(Tins::PDU::PDUType::DOT11)) {
auto dot11PDU = dynamic_cast<Tins::Dot11*>(pdu);
auto dst = dot11PDU->addr1().to_string();
this->hosts.insert(dst);
this->hostsInfo[dst].packetsReceived += 1;
}
}
pdu = pdu->release_inner_pdu();
}
}
} catch (std::exception& ) {}
if (!packetTimestamps.empty()) {
this->scanDates.emplace_back(
std::make_pair(packetTimestamps.front(), packetTimestamps.back())
);
}
}
for (auto& entry : this->availability) {
entry.second.sort();
entry.second.unique();
}
sqlite3* db;
auto memorySqlite = sqlite3_open(":memory:", &db);
if (memorySqlite != SQLITE_OK) {
throw "Could not open in-memory database";
}
sqlite3* localDb;
auto localSqlite = sqlite3_open((this->dbPath + getNow() + ".sqlite").c_str(), &localDb);
if (memorySqlite != SQLITE_OK) {
throw "Could not open database file";
}
std::cout << "Adding hosts..";
this->addHosts(db);
std::cout << "Done\n";
std::cout << "Adding connections..";
this->addConnections(db);
std::cout << "Done\n";
std::cout << "Adding host info..";
this->addHostInfo(db);
std::cout << "Done\n";
std::cout << "Adding packet types..";
this->addPacketTypes(db);
std::cout << "Done\n";
std::cout << "Adding timestamps..";
this->addTimestamps(db);
std::cout << "Done\n";
std::cout << "Adding scanning times..";
this->addScanTimes(db);
std::cout << "Done\n";
auto dbFile = sqlite3_backup_init(localDb, "main", db, "main");
if (dbFile) {
sqlite3_backup_step(dbFile, -1);
sqlite3_backup_finish(dbFile);
}
sqlite3_close(db);
}
}

62
netvu/src/CMakeLists.txt Normal file
View File

@ -0,0 +1,62 @@
cmake_minimum_required(VERSION 3.16)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
project(netvu VERSION 1.0)
add_executable(netvu netvu.cpp)
find_package(Boost REQUIRED COMPONENTS
program_options
filesystem
system
)
if(Boost_FOUND)
list(APPEND EXTRA_LIBS
Boost::program_options
Boost::filesystem
Boost::system
)
endif()
# set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
# set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
# set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
# option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
option(USE_SCANNER "Use 802.11 scanner" ON)
option(USE_ANALYZER "Use pcap files analyzer" ON)
option(USE_SERVER "Use listen server to interact with database" ON)
if(USE_SCANNER)
add_subdirectory(Scanner)
list(APPEND EXTRA_LIBS Scanner)
endif()
if(USE_ANALYZER)
add_subdirectory(Analyzer)
list(APPEND EXTRA_LIBS Analyzer)
endif()
if(USE_SERVER)
add_subdirectory(Server)
list(APPEND EXTRA_LIBS Server)
endif()
configure_file(netvuconfig.h.in netvuconfig.h)
target_link_libraries(netvu PUBLIC ${EXTRA_LIBS} )
target_include_directories(
netvu PUBLIC
"${PROJECT_BINARY_DIR}"
)
install(TARGETS netvu DESTINATION bin)
install(
FILES "${PROJECT_BINARY_DIR}/netvuconfig.h"
DESTINATION include
)

View File

@ -0,0 +1,53 @@
add_library(Scanner WifiScanner.cpp)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
find_package(Boost REQUIRED COMPONENTS
thread
filesystem
system
)
if(Boost_FOUND)
list(APPEND EXTRA_LIBS
Boost::thread
Boost::filesystem
Boost::system
)
endif()
find_package(libtins)
if(libtins_FOUND)
list(APPEND EXTRA_LIBS
tins
)
else()
find_library(TINS_LIBRARY tins REQUIRED)
if(TINS_LIBRARY)
list(APPEND EXTRA_LIBS
tins
)
endif()
endif()
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
if(Threads_FOUND)
list(APPEND EXTRA_LIBS
Threads::Threads
)
endif()
target_include_directories(
Scanner
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(netvu PUBLIC ${EXTRA_LIBS})
install(TARGETS Scanner DESTINATION lib)
install(FILES Scanner.h DESTINATION include)

View File

@ -0,0 +1,49 @@
#ifndef SCANNER_H
#define SCANNER_H
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <tins/packet_writer.h>
#include <tins/sniffer.h>
namespace Scanner {
std::string getPcapPath();
class WifiScanner {
bool radioTap;
bool monitorAlreadyEnabled = false;
std::string interface;
std::string logDir;
std::uint32_t channelHopInterval;
std::uint32_t maxPackets;
std::uint32_t packetCount = 0;
std::vector<std::uint8_t> channels;
std::unique_ptr<Tins::Sniffer> sniffer;
std::unique_ptr<Tins::PacketWriter> packetWriter;
bool sniffCallback(Tins::Packet& packet);
bool isRadioTap(const std::string& interfaceName);
bool setupMonitorMode();
bool teardownMonitorMode();
std::string findDefaultInterface();
std::vector<std::uint8_t> getInterfaceChannels(const std::string& interfaceName);
void channelHopper(
const std::string& interface,
const std::vector<std::uint8_t>& channels,
std::uint32_t channelHopInterval
);
public:
WifiScanner(
const std::string& interface = "",
const std::vector<std::string>& channels = {},
const std::string& maxPackets = "0",
const std::string& logDir = "log",
const std::string& channelHopInterval = "300"
);
~WifiScanner();
void start();
};
};
#endif

View File

@ -0,0 +1,340 @@
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <ctime>
#include <filesystem>
#include <functional>
#include <iomanip>
#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
#include <boost/process.hpp>
#include <boost/process/system.hpp>
#include <boost/thread/thread.hpp>
#include <thread>
#include <tins/network_interface.h>
#include <tins/packet.h>
#include <tins/sniffer.h>
#include <tins/tins.h>
#include "Scanner.h"
namespace Scanner {
namespace bp = boost::process;
namespace fs = std::filesystem;
std::string getPcapPath() {
auto now = std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now()
);
auto localNow = *std::localtime(&now);
std::ostringstream localTimeString;
localTimeString << std::put_time(&localNow, "%F %T");
auto locString = localTimeString.str();
std::replace(
locString.begin(),
locString.end(),
' ',
'_'
);
std::replace(
locString.begin(),
locString.end(),
':',
'-'
);
locString += ".pcap";
return locString;
}
WifiScanner::WifiScanner(
const std::string& interface,
const std::vector<std::string>& channels,
const std::string& maxPackets,
const std::string& logDir,
const std::string& channelHopInterval
) {
this->interface = interface;
if (this->interface.empty()) {
this->interface = findDefaultInterface();
if (this->interface.empty()) {
throw std::runtime_error("No wireless interface was found");
}
}
std::cout << "Using " << this->interface << '\n';
bool setMonitor = this->setupMonitorMode();
if (!setMonitor) {
throw std::runtime_error("Failed to set Monitor mode on this interface");
}
if (channels.empty() || channels.at(0) == "0") {
this->channels = this->getInterfaceChannels(this->interface);
if (this->channels.empty()) {
throw std::runtime_error("No channels were found for this interface");
}
} else {
for (auto ch : channels) {
this->channels.emplace_back(std::stoul(ch));
}
}
this->maxPackets = std::stoul(maxPackets);
this->logDir = logDir;
if (!fs::exists(this->logDir)) {
auto res = fs::create_directory(this->logDir);
if (!res) {
std::cerr << "Failed to create " << this->logDir << " - using .\n";
this->logDir = ".";
}
}
this->logDir = (fs::path(this->logDir) / getPcapPath()).string();
if (isRadioTap(this->interface)) {
this->packetWriter = std::make_unique<Tins::PacketWriter>(
this->logDir,
Tins::DataLinkType<Tins::RadioTap>()
);
} else {
this->packetWriter = std::make_unique<Tins::PacketWriter>(
this->logDir,
Tins::DataLinkType<Tins::Dot11>()
);
}
std::stoul(channelHopInterval) < 300
? this->channelHopInterval = 300
: this->channelHopInterval = std::stoul(channelHopInterval);
Tins::SnifferConfiguration snifferConfiguration;
snifferConfiguration.set_promisc_mode(true);
snifferConfiguration.set_rfmon(true);
this->sniffer = std::make_unique<Tins::Sniffer>(
this->interface,
snifferConfiguration
);
}
WifiScanner::~WifiScanner() {
if (!this->monitorAlreadyEnabled) {
this->teardownMonitorMode();
}
}
bool WifiScanner::sniffCallback(Tins::Packet& packet) {
this->packetWriter->write(packet);
this->packetCount += 1;
std::cout << "Packet: " << std::to_string(this->packetCount) ;
if (this->maxPackets) {
std::cout << '/' << std::to_string(this->maxPackets);
}
std::cout << '\n';
return true;
}
bool WifiScanner::isRadioTap(const std::string& intName) {
bp::ipstream is;
bp::child c(bp::search_path("ip"), "link", "show", intName,
bp::std_out > is);
std::string line;
std::vector<std::string> strs;
bool yes = false;
while (c.running() && std::getline(is, line) && !line.empty()) {
if (line.find("radiotap") != std::string::npos) {
yes = true;
break;
}
}
c.wait();
return yes;
}
std::string WifiScanner::findDefaultInterface() {
bp::ipstream is;
bp::child c(bp::search_path("iwconfig"), bp::std_out > is);
std::string interface;
std::string line;
while (c.running() && std::getline(is, line) && !line.empty()) {
if (line.find("802.11") != std::string::npos) {
std::vector<std::string> strs;
boost::split(strs, line, boost::is_any_of("\t "));
interface = strs.front();
break;
}
}
c.wait();
return interface;
}
bool WifiScanner::setupMonitorMode() {
bp::ipstream is;
bp::child c(bp::search_path("iwconfig"), this->interface, bp::std_out > is);
std::string line;
auto isEnabled = false;
while (c.running() && std::getline(is, line) && !line.empty()) {
if (line.find("Mode:Monitor") != std::string::npos) {
isEnabled = true;
break;
}
break;
}
c.wait();
if (isEnabled) {
this->monitorAlreadyEnabled = true;
std::cout<< "Monitor mode already enabled\n";
return true;
}
std::cout << "Turning " << this->interface << " down\n";
bp::child ifconfigDown(bp::search_path("ifconfig"), this->interface, "down");
ifconfigDown.wait();
if (ifconfigDown.exit_code()) {
return false;
}
std::cout << "Enabling Monitor mode\n";
bp::child iwconfigMonitor(bp::search_path("iwconfig"), this->interface, "mode", "Monitor");
iwconfigMonitor.wait();
if (iwconfigMonitor.exit_code()) {
return false;
}
std::cout << "Turning " << this->interface << " up\n";
bp::child ifconfigUp(bp::search_path("ifconfig"), this->interface, "up");
ifconfigUp.wait();
if (ifconfigUp.exit_code()) {
return false;
}
return true;
}
bool WifiScanner::teardownMonitorMode() {
bp::ipstream is;
bp::child c(bp::search_path("iwconfig"), this->interface, bp::std_out > is);
std::cout << "Turning " << this->interface << " down\n";
bp::child ifconfigDown(bp::search_path("ifconfig"), this->interface, "down");
ifconfigDown.wait();
if (ifconfigDown.exit_code()) {
return false;
}
std::cout << "Enabling Managed mode\n";
bp::child iwconfigMonitor(bp::search_path("iwconfig"), this->interface, "mode", "Managed");
iwconfigMonitor.wait();
if (iwconfigMonitor.exit_code()) {
return false;
}
std::cout << "Turning " << this->interface << "up\n";
bp::child ifconfigUp(bp::search_path("ifconfig"), this->interface, "up");
ifconfigUp.wait();
if (ifconfigUp.exit_code()) {
return false;
}
std::cout << "All done\n";
return true;
}
std::vector<std::uint8_t> WifiScanner::getInterfaceChannels(const std::string& interfaceName) {
bp::ipstream is;
bp::child c(bp::search_path("iwlist"), interfaceName, "channel", bp::std_out > is);
std::string line;
std::vector<std::string> strs;
while (c.running() && std::getline(is, line) && !line.empty()) {
if (line.find(interfaceName) != std::string::npos) {
boost::split(strs, line, boost::is_any_of("\t "));
break;
}
}
c.wait();
std::vector<std::uint8_t> channels;
if (strs.empty()) {
return channels;
}
std::uint8_t channelCount = 0;
for (auto str : strs) {
if (str.length() != 0) {
auto isInt = std::all_of(std::begin(str), std::end(str), [](char i) {
return std::isdigit(i);
});
if (isInt) {
channelCount = std::stoul(str);
break;
}
}
}
if (channelCount == 0) {
return channels;
}
for (std::uint8_t i = 1; i <= channelCount; ++i) {
channels.emplace_back(i);
}
return channels;
}
void WifiScanner::channelHopper(
const std::string& interface,
const std::vector<std::uint8_t>& channels,
std::uint32_t channelHopInterval
) {
auto currentChannel = 0;
for (;;) {
currentChannel += 1;
if (currentChannel == channels.size()) {
currentChannel = 0;
}
bp::ipstream is;
bp::child c(
bp::search_path("iwconfig"),
interface,
"channel",
std::to_string(channels.at(currentChannel)),
bp::std_out > is
);
c.wait();
std::this_thread::sleep_for(std::chrono::milliseconds(channelHopInterval));
}
}
void WifiScanner::start() {
boost::thread channelHoppingThread(
std::bind(&WifiScanner::channelHopper, this,
this->interface, this->channels, this->channelHopInterval)
);
this->sniffer->sniff_loop(
std::bind(&WifiScanner::sniffCallback, this, std::placeholders::_1), this->maxPackets
);
}
}

View File

@ -0,0 +1,46 @@
add_library(Server HttpServer.cpp)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
find_package(Boost REQUIRED COMPONENTS
thread
)
if(Boost_FOUND)
list(APPEND EXTRA_LIBS
Boost::thread
)
endif()
find_package(SQLite3 REQUIRED)
if(SQLite3_FOUND)
list(APPEND EXTRA_LIBS
SQLite::SQLite3
)
endif()
find_package(nlohmann_json REQUIRED)
if(nlohmann_json_FOUND)
list(APPEND EXTRA_LIBS
nlohmann_json
)
endif()
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
if(Threads_FOUND)
list(APPEND EXTRA_LIBS
Threads::Threads
)
endif()
target_include_directories(
Server
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(netvu PUBLIC ${EXTRA_LIBS})
install(TARGETS Server DESTINATION lib)
install(FILES Server.h DESTINATION include)

View File

@ -0,0 +1,270 @@
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <filesystem>
#include <functional>
#include <iostream>
#include <iterator>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
#include <boost/asio.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <sqlite3.h>
#include <nlohmann/json.hpp>
#include "Server.h"
using json = nlohmann::json;
bool isInteger(const std::string& s) {
return std::all_of(
std::begin(s),
std::end(s),
[](char i) {
return isdigit(i);
}
);
}
bool isReal(const std::string& s) {
char* end = nullptr;
auto val = std::strtod(s.c_str(), &end);
return end != s.c_str() && *end == '\0' && val != std::numeric_limits<double>::max();
}
int jsonCustomer(
void* resp,
int argc,
char** argv,
char** colname) {
auto respp = static_cast<json *>(resp);
json o;
for (int i = 0; i < argc; i++) {
auto isInt = isInteger(argv[i]);
if (isInt) {
o[colname[i]] = std::stoi(argv[i]);
continue;
}
auto isNumeric = isReal(argv[i]);
if (isNumeric) {
o[colname[i]] = std::stod(argv[i]);
continue;
}
o[colname[i]] = argv[i];
}
(*respp).emplace_back(o);
return 0;
}
namespace Server {
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
namespace fs = std::filesystem;
using tcp = boost::asio::ip::tcp;
class HttpServer::HttpConnection : public std::enable_shared_from_this<HttpConnection> {
public:
HttpConnection(
tcp::socket socket,
const std::string& dbPath,
const std::string& cors
) : socket(std::move(socket)) {
this->dbPath = dbPath;
this->cors = cors;
}
void open() {
readRequest();
handleDeadline();
}
private:
std::string dbPath;
std::string cors;
tcp::socket socket;
beast::flat_buffer buffer{8192};
http::request<http::dynamic_body> request;
http::response<http::dynamic_body> response;
net::steady_timer deadline{socket.get_executor(), std::chrono::seconds(30)};
json getData(std::string sqlQuery) {
sqlite3* db;
auto sql = sqlQuery.data();
char *err_msg = 0;
int rc = sqlite3_open(this->dbPath.c_str(), &db);
json j;
rc = sqlite3_exec(db, sql, jsonCustomer, &j, &err_msg);
if (rc != SQLITE_OK) {
std::cerr << err_msg;
}
sqlite3_close(db);
return j;
}
void readRequest() {
auto self = shared_from_this();
http::async_read(socket, buffer,
request,
[self](beast::error_code errorCode,
std::size_t bytes_transferred
) {
boost::ignore_unused(bytes_transferred);
if(!errorCode) {
self->processRequest();
}
});
}
void processRequest() {
response.version(request.version());
response.keep_alive(false);
if (request.method() == http::verb::get) {
response.result(http::status::ok);
response.set(http::field::access_control_allow_origin, this->cors);
createResponse();
} else {
response.result(http::status::bad_request);
}
writeResponse();
}
void createResponse() {
auto target = request.target();
json data;
if (target == "/hosts") {
data = getData("SELECT * FROM hosts;");
}
if (target == "/connections") {
data = getData("SELECT * FROM connections;");
}
if (target == "/info") {
data = getData("SELECT * FROM info;");
}
if (target == "/packet-types") {
data = getData("SELECT * FROM packet_types;");
}
if (target == "/availability") {
data = getData("SELECT * FROM availability;");
}
if (target == "/scan-dates") {
data = getData("SELECT * FROM scan_dates;");
}
if (data.empty()) {
response.result(http::status::not_found);
response.set(http::field::content_type, "text/plain");
beast::ostream(response.body()) << "Not found\r\n";
} else {
response.result(http::status::ok);
response.set(http::field::content_type, "application/json");
beast::ostream(response.body()) << data.dump();
}
}
void writeResponse() {
auto self = shared_from_this();
response.content_length(response.body().size());
http::async_write(socket, response, [self](beast::error_code ec, std::size_t) {
self->socket.shutdown(tcp::socket::shutdown_send, ec);
self->deadline.cancel();
}
);
}
void handleDeadline() {
auto self = shared_from_this();
deadline.async_wait([self](beast::error_code ec) {
if(!ec) {
self->socket.close(ec);
}
});
}
};
HttpServer::HttpServer(
const std::string& dbPath,
const std::string& addr,
const std::string& port,
const std::string& cors
) {
this->dbPath = dbPath;
this->addr = net::ip::make_address(addr);
this->port = std::stoul(port);
this->cors = cors;
if (!fs::exists(this->dbPath)) {
throw std::runtime_error("Specified path does not exist");
}
if (fs::is_directory(this->dbPath)) {
auto dir = fs::directory_iterator(this->dbPath);
std::vector<std::string> entries;
for (auto entry : dir) {
if (entry.is_regular_file()) {
if (entry.path().extension().string() == ".sqlite") {
entries.emplace_back(entry.path().string());
}
}
}
std::sort(entries.begin(), entries.end(), std::greater<std::string>());
if (entries.empty()) {
throw std::runtime_error("No database files found");
}
this->dbPath = entries.front();
} else {
throw std::runtime_error("Invalid database path");
}
}
void HttpServer::setup(tcp::acceptor& acceptor, tcp::socket& socket) {
acceptor.async_accept(socket, [&](beast::error_code errorCode) {
if(!errorCode) {
std::make_shared<HttpConnection>(std::move(socket), this->dbPath, this->cors)->open();
}
setup(acceptor, socket);
});
}
void HttpServer::start() {
net::io_context ioc{1};
tcp::acceptor acceptor{ioc, {this->addr, this->port}};
tcp::socket socket{ioc};
setup(acceptor, socket);
ioc.run();
}
}

33
netvu/src/Server/Server.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef SERVER_H
#define SERVER_H
#include <cstdint>
#include <string>
#include <boost/asio.hpp>
#include <sqlite3.h>
namespace Server {
class HttpServer {
class HttpConnection;
boost::asio::ip::address addr;
std::uint16_t port;
std::string dbPath;
std::string cors;
void setup(
boost::asio::ip::tcp::acceptor& acceptor,
boost::asio::ip::tcp::socket& socket
);
public:
HttpServer(
const std::string& dbPath = "db",
const std::string& addr = "0.0.0.0",
const std::string& port = "9000",
const std::string& cors = "*"
);
void start();
};
};
#endif

231
netvu/src/netvu.cpp Normal file
View File

@ -0,0 +1,231 @@
#include <algorithm>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <filesystem>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
#include <boost/program_options.hpp>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/program_options/value_semantic.hpp>
#include <boost/program_options/variables_map.hpp>
#include "netvuconfig.h"
#ifdef USE_SCANNER
# include "Scanner.h"
#endif
#ifdef USE_ANALYZER
# include "Analyzer.h"
#endif
#ifdef USE_SERVER
# include "Server.h"
#endif
std::string launchMode;
void signalHandler(int signal);
int main(int argc, char **argv) {
namespace po = boost::program_options;
namespace fs = std::filesystem;
po::options_description desc("Allowed options");
desc.add_options()
(
"help,h",
"print usage information and exit"
)
(
"config,c",
po::value<std::string>()->default_value("netvu.conf")->required(),
"alternative config file"
);
po::options_description config("Configuration");
config.add_options()
(
"mode,m",
po::value<std::string>(),
"runtime mode - scanner/analyzer/server"
);
po::options_description scannerConfig("Scanner");
scannerConfig.add_options()
(
"scanner.interface,i",
po::value<std::string>(),
"wireless interface"
)
(
"scanner.channels,C",
po::value< std::vector<std::string> >()->composing()->multitoken(),
"space separated 802.11 channels numbers"
)
(
"scanner.max_packets,M",
po::value< std::string >()->default_value("0"),
"packet capture limit"
)
(
"scanner.log_dir,l",
po::value<std::string>()->default_value("log")->required(),
"where to save captured packets"
)
(
"scanner.channel_hop_interval,I",
po::value<std::string>()->default_value("300")->required(),
"channel hop interval in milliseconds"
);
config.add(scannerConfig);
po::options_description analyzerConfig("Analyzer");
analyzerConfig.add_options()
(
"analyzer.log_dir,L",
po::value<std::string>()->default_value("log"),
"where captured packets are stored"
)
(
"analyzer.database_dir,D",
po::value<std::string>()->default_value("db"),
"where to save analisys result"
);
config.add(analyzerConfig);
po::options_description serverConfig("Server");
serverConfig.add_options()
(
"server.address,a",
po::value<std::string>()->default_value("0.0.0.0"),
"listening address"
)
(
"server.port,p",
po::value<std::string>()->default_value("9000"),
"listening port"
)
(
"server.database_dir,d",
po::value<std::string>()->default_value("db"),
"where analisys result is stored"
)
(
"server.cors,o",
po::value<std::string>()->default_value("*"),
"access control allow origin"
);
config.add(serverConfig);
desc.add(config);
try {
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm);
if (fs::exists(vm["config"].as<std::string>())) {
po::store(
po::parse_config_file(vm["config"].as<std::string>().data(), config),
vm
);
}
po::notify(vm);
if (vm.count("help")) {
std::cout << desc << "\n";
return EXIT_SUCCESS;
}
std::signal(SIGINT, signalHandler);
if (!vm.count("mode")) {
std::cout << desc << "\n";
return EXIT_SUCCESS;
}
auto mode = vm["mode"].as<std::string>();
if (mode == "scanner") {
#ifdef USE_SCANNER
std::cout << "Starting scanner\n";
launchMode = "scanner";
std::string interface;
if (vm.count("scanner.interface")) {
interface = vm["scanner.interface"].as<std::string>();
}
std::vector<std::string> channels;
if (vm.count("scanner.channels")) {
channels = vm["scanner.channels"].as<std::vector<std::string>>();
}
auto maxPackets = vm["scanner.max_packets"].as<std::string>();
auto logDir = vm["scanner.log_dir"].as<std::string>();
auto channelHopInterval = vm["scanner.channel_hop_interval"].as<std::string>();
auto wifiScanner = Scanner::WifiScanner(
interface,
channels,
maxPackets,
logDir,
channelHopInterval
);
wifiScanner.start();
#else
throw std::runtime_error("Netvu was compiled without Scanner support");
#endif
} else if (mode == "analyzer") {
#ifdef USE_ANALYZER
std::cout << "Starting analyzer\n";
launchMode = "analyzer";
auto logDir = vm["analyzer.log_dir"].as<std::string>();
auto databaseDir = vm["analyzer.database_dir"].as<std::string>();
auto pcapAnalyzer = Analyzer::PCAPAnalyzer(logDir, databaseDir);
pcapAnalyzer.start();
#else
throw std::runtime_error("Netvu was compiled without Analyzer support");
#endif
} else if (mode == "server") {
#ifdef USE_SERVER
std::cout << "Starting server\n";
launchMode = "server";
auto address = vm["server.address"].as<std::string>();
auto port = vm["server.port"].as<std::string>();
auto databaseDir = vm["server.database_dir"].as<std::string>();
auto cors = vm["server.cors"].as<std::string>();
auto httpServer = Server::HttpServer(databaseDir, address, port, cors);
httpServer.start();
#else
throw std::runtime_error("Netvu was compiled without Server support");
#endif
} else {
std::cerr << "Invalid runtime mode specified\n";
return EXIT_FAILURE;
}
} catch (const std::exception& ex) {
std::cerr << ex.what() << std::endl;
return EXIT_FAILURE;
}
}
void signalHandler(int signal) {
std::cout << "Stopping " + launchMode << '\n';
std::exit(2);
}

View File

@ -0,0 +1,6 @@
#define netvu_VERSION_MAJOR @netvu_VERSION_MAJOR@
#define netvu_VERSION_MINOR @netvu_VERSION_MINOR@
#cmakedefine USE_SCANNER
#cmakedefine USE_ANALYZER
#cmakedefine USE_SERVER