hiveguard-backend
Version:
Backend for HiveGuard
1,345 lines (1,281 loc) • 40.7 kB
JavaScript
/*
* Copyright 2021-2022 Dimitrios-Georgios Akestoridis
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require('dotenv').config();
const express = require('express');
const { Pool } = require('pg');
const nodemailer = require('nodemailer');
const {
isValidWIDSSensorID,
isValidHours,
isValidAlertID,
} = require('./validations');
const defaults = require('./defaults.json');
class InspectionServer {
constructor(config = {}) {
this.lastNWKAUXSeqnums = {};
this.inspectionIPAddress = (
config.inspectionIPAddress || defaults.inspectionIPAddress
);
this.inspectionPortNumber = (
config.inspectionPortNumber || defaults.inspectionPortNumber
);
this.inspectionDelay = (
config.inspectionDelay || defaults.inspectionDelay
);
this.notificationCooldown = (
config.notificationCooldown || defaults.notificationCooldown
);
this.app = express();
this.app.use(express.json());
this.app.use(
(req, res, next) => {
res.setHeader(
'Access-Control-Allow-Origin',
config.originURL || defaults.originURL,
);
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type',
);
res.setHeader(
'Access-Control-Allow-Methods',
'GET, OPTIONS, PUT',
);
next();
},
);
this.pool = new Pool(
{
host: config.databaseIPAddress || defaults.databaseIPAddress,
port: config.databasePortNumber || defaults.databasePortNumber,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASS,
},
);
this.transporter = nodemailer.createTransport(
{
host: process.env.EMAIL_SNDR_HOST,
port: process.env.EMAIL_SNDR_PORT,
secure: true,
auth: {
user: process.env.EMAIL_SNDR_ADDR,
pass: process.env.EMAIL_SNDR_PASS,
},
},
);
this.app.get(
'/api/wids-sensors',
async (req, res, next) => {
try {
const result = await this.pool.query(
'SELECT wids_sensor_id, wids_sensor_api FROM wids_sensors '
+ 'ORDER BY wids_sensor_id',
);
res.json(result.rows);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/wids-sensors/:id/cpu',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.params.id)
|| !isValidHours(req.query.hours)
) {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT epoch_timestamp, cpu_percent '
+ 'FROM wids_utilization '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'ORDER BY epoch_timestamp',
[
req.params.id,
req.query.hours,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.cpu_percent,
}
),
),
);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/wids-sensors/:id/memory',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.params.id)
|| !isValidHours(req.query.hours)
) {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT epoch_timestamp, memory_percent '
+ 'FROM wids_utilization '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'ORDER BY epoch_timestamp',
[
req.params.id,
req.query.hours,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.memory_percent,
}
),
),
);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/wids-sensors/:id/disk',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.params.id)
|| !isValidHours(req.query.hours)
) {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT epoch_timestamp, disk_percent '
+ 'FROM wids_utilization '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'ORDER BY epoch_timestamp',
[
req.params.id,
req.query.hours,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.disk_percent,
}
),
),
);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/nearby-networks',
async (req, res, next) => {
try {
const panidResult = await this.pool.query(
'SELECT DISTINCT panid FROM wids_networks ORDER BY panid',
);
const nearbyNetworks = await Promise.all(
panidResult.rows.map(
(row) => new Promise(
(resolve, reject) => {
this.pool.query(
'SELECT DISTINCT epidset FROM wids_networks '
+ 'WHERE panid=$1 AND epidset!=$2',
[
row.panid,
'',
],
(err, epidResult) => {
if (err) {
reject(err);
} else if (epidResult.rows.length === 0) {
resolve(
{
panid: row.panid,
epid: 'Unknown',
},
);
} else if (epidResult.rows.length === 1) {
if (epidResult.rows[0].epidset.includes(';')) {
resolve(
{
panid: row.panid,
epid: 'Conflicting Data',
},
);
} else {
resolve(
{
panid: row.panid,
epid: epidResult.rows[0].epidset,
},
);
}
} else {
resolve(
{
panid: row.panid,
epid: 'Conflicting Data',
},
);
}
},
);
},
),
),
);
res.json(nearbyNetworks);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/pan-identifiers',
async (req, res, next) => {
try {
const result = await this.pool.query(
'SELECT DISTINCT panid FROM wids_networks ORDER BY panid',
);
res.json(Array.from(result.rows, (row) => row.panid));
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/topology/:id',
async (req, res, next) => {
if (!isValidHours(req.query.hours)) {
res.sendStatus(400);
return;
}
try {
const shortAddrResult = await this.pool.query(
'SELECT DISTINCT shortaddr FROM wids_short_addresses '
+ 'WHERE panid=$1 ORDER BY shortaddr',
[
req.params.id,
],
);
const tableRows = await Promise.all(
shortAddrResult.rows.map(
(row) => new Promise(
(resolve, reject) => {
this.pool.query(
'SELECT DISTINCT altset, nwkset '
+ 'FROM wids_short_addresses '
+ 'WHERE panid=$1 AND shortaddr=$2',
[
req.params.id,
row.shortaddr,
],
(err, setsResult) => {
if (err) {
reject(err);
} else {
let extendedaddr = '';
let nwkdevtype = '';
if (setsResult.rows.length === 0) {
extendedaddr = 'Unknown';
nwkdevtype = 'Unknown';
} else if (setsResult.rows.length === 1) {
if (setsResult.rows[0].altset === '') {
extendedaddr = 'Unknown';
} else if (
setsResult.rows[0].altset.includes(';')
) {
extendedaddr = 'Conflicting Data';
} else {
extendedaddr = setsResult.rows[0].altset;
}
if (setsResult.rows[0].nwkset === '') {
nwkdevtype = 'Unknown';
} else if (
setsResult.rows[0].nwkset.includes(';')
) {
nwkdevtype = 'Conflicting Data';
} else {
nwkdevtype = setsResult.rows[0].nwkset;
}
} else {
const extendedaddrSet = new Set();
const nwkdevtypeSet = new Set();
setsResult.rows.forEach(
(element) => {
if (element.altset !== '') {
extendedaddrSet.add(element.altset);
}
if (element.nwkset !== '') {
nwkdevtypeSet.add(element.nwkset);
}
},
);
if (extendedaddrSet.size === 0) {
extendedaddr = 'Unknown';
} else if (extendedaddrSet.size === 1) {
// eslint-disable-next-line prefer-destructuring
extendedaddr = Array.from(extendedaddrSet)[0];
if (extendedaddr.includes(';')) {
extendedaddr = 'Conflicting Data';
}
} else {
extendedaddr = 'Conflicting Data';
}
if (nwkdevtypeSet.size === 0) {
nwkdevtype = 'Unknown';
} else if (nwkdevtypeSet.size === 1) {
// eslint-disable-next-line prefer-destructuring
nwkdevtype = Array.from(nwkdevtypeSet)[0];
if (nwkdevtype.includes(';')) {
nwkdevtype = 'Conflicting Data';
}
} else {
nwkdevtype = 'Conflicting Data';
}
}
resolve(
{
shortaddr: row.shortaddr,
extendedaddr,
nwkdevtype,
},
);
}
},
);
},
),
),
);
const pairsResult = await this.pool.query(
'SELECT DISTINCT srcaddr, dstaddr '
+ 'FROM wids_pairs '
+ 'WHERE panid=$1 '
+ 'AND latest>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2)',
[
req.params.id,
req.query.hours,
],
);
let graphDef = 'digraph {\n';
tableRows.forEach(
(row) => {
graphDef += `\t"${row.shortaddr}" [color=black style=filled `;
if (row.nwkdevtype === 'Zigbee Coordinator') {
graphDef += 'fillcolor="#FF0000" ';
} else if (row.nwkdevtype === 'Zigbee Router') {
graphDef += 'fillcolor="#FFA500" ';
} else if (row.nwkdevtype === 'Zigbee End Device') {
graphDef += 'fillcolor="#FFFF00" ';
} else {
graphDef += 'fillcolor="#FFFFFF" ';
}
graphDef += 'fontname="Liberation Sans" fontsize=14]\n';
},
);
pairsResult.rows.forEach(
(row) => {
graphDef += `\t"${row.srcaddr}" -> "${row.dstaddr}"\n`;
},
);
graphDef += '}\n';
res.json(
{
table: tableRows,
graph: graphDef,
},
);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/short-addresses',
async (req, res, next) => {
try {
const result = await this.pool.query(
'SELECT DISTINCT shortaddr FROM wids_short_addresses '
+ 'WHERE panid=$1 ORDER BY shortaddr',
[
req.query.panid,
],
);
res.json(Array.from(result.rows, (row) => row.shortaddr));
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/packet-counters',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.query.sensor)
|| !isValidHours(req.query.hours)
|| !req.query.srcpanid
) {
res.sendStatus(400);
return;
}
try {
if (req.query.srcshortaddr) {
const result = await this.pool.query(
'SELECT epoch_timestamp, packet_counter '
+ 'FROM wids_packet_counters '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'AND srcpanid=$3 AND srcshortaddr=$4 '
+ 'ORDER BY epoch_timestamp',
[
req.query.sensor,
req.query.hours,
req.query.srcpanid,
req.query.srcshortaddr,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.packet_counter,
}
),
),
);
} else {
const result = await this.pool.query(
'SELECT epoch_timestamp, packet_counter '
+ 'FROM wids_packet_counters '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'AND srcpanid=$3 AND srcshortaddr IS NULL '
+ 'ORDER BY epoch_timestamp',
[
req.query.sensor,
req.query.hours,
req.query.srcpanid,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.packet_counter,
}
),
),
);
}
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/byte-counters',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.query.sensor)
|| !isValidHours(req.query.hours)
|| !req.query.srcpanid
) {
res.sendStatus(400);
return;
}
try {
if (req.query.srcshortaddr) {
const result = await this.pool.query(
'SELECT epoch_timestamp, byte_counter '
+ 'FROM wids_byte_counters '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'AND srcpanid=$3 AND srcshortaddr=$4 '
+ 'ORDER BY epoch_timestamp',
[
req.query.sensor,
req.query.hours,
req.query.srcpanid,
req.query.srcshortaddr,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.byte_counter,
}
),
),
);
} else {
const result = await this.pool.query(
'SELECT epoch_timestamp, byte_counter '
+ 'FROM wids_byte_counters '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'AND srcpanid=$3 AND srcshortaddr IS NULL '
+ 'ORDER BY epoch_timestamp',
[
req.query.sensor,
req.query.hours,
req.query.srcpanid,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.byte_counter,
}
),
),
);
}
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/mac-seqnum',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.query.sensor)
|| !isValidHours(req.query.hours)
|| !req.query.srcpanid
|| !req.query.srcshortaddr
) {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT epoch_timestamp, mac_seqnum '
+ 'FROM wids_mac_seqnums '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'AND srcpanid=$3 AND srcshortaddr=$4 '
+ 'ORDER BY epoch_timestamp',
[
req.query.sensor,
req.query.hours,
req.query.srcpanid,
req.query.srcshortaddr,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.mac_seqnum,
}
),
),
);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/beacon-seqnum',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.query.sensor)
|| !isValidHours(req.query.hours)
|| !req.query.srcpanid
|| !req.query.srcshortaddr
) {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT epoch_timestamp, beacon_seqnum '
+ 'FROM wids_beacon_seqnums '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'AND srcpanid=$3 AND srcshortaddr=$4 '
+ 'ORDER BY epoch_timestamp',
[
req.query.sensor,
req.query.hours,
req.query.srcpanid,
req.query.srcshortaddr,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.beacon_seqnum,
}
),
),
);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/nwk-seqnum',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.query.sensor)
|| !isValidHours(req.query.hours)
|| !req.query.srcpanid
|| !req.query.srcshortaddr
) {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT epoch_timestamp, nwk_seqnum '
+ 'FROM wids_nwk_seqnums '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'AND srcpanid=$3 AND srcshortaddr=$4 '
+ 'ORDER BY epoch_timestamp',
[
req.query.sensor,
req.query.hours,
req.query.srcpanid,
req.query.srcshortaddr,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.nwk_seqnum,
}
),
),
);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/nwkaux-seqnum',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.query.sensor)
|| !isValidHours(req.query.hours)
|| !req.query.srcpanid
|| !req.query.srcshortaddr
) {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT epoch_timestamp, nwkaux_seqnum '
+ 'FROM wids_nwkaux_seqnums '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'AND srcpanid=$3 AND srcshortaddr=$4 '
+ 'ORDER BY epoch_timestamp',
[
req.query.sensor,
req.query.hours,
req.query.srcpanid,
req.query.srcshortaddr,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.nwkaux_seqnum,
}
),
),
);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/battery-percentages',
async (req, res, next) => {
if (
!isValidWIDSSensorID(req.query.sensor)
|| !isValidHours(req.query.hours)
|| !req.query.srcpanid
|| !req.query.srcshortaddr
) {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT epoch_timestamp, percentage '
+ 'FROM wids_battery_percentages '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND epoch_timestamp>=EXTRACT(EPOCH FROM NOW() '
+ '- INTERVAL \'1 HOUR\' * $2) '
+ 'AND srcpanid=$3 AND srcshortaddr=$4 '
+ 'ORDER BY epoch_timestamp',
[
req.query.sensor,
req.query.hours,
req.query.srcpanid,
req.query.srcshortaddr,
],
);
res.json(
result.rows.map(
(row) => (
{
x: parseFloat(row.epoch_timestamp) * 1000.0,
y: row.percentage,
}
),
),
);
} catch (err) {
next(err);
}
},
);
this.app.get(
'/api/alerts',
async (req, res, next) => {
if (req.query.archived !== 'true' && req.query.archived !== 'false') {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT alert_id, message FROM nsm_alerts WHERE archived=$1 '
+ 'ORDER BY alert_id DESC',
[
(req.query.archived === 'true'),
],
);
res.json(result.rows);
} catch (err) {
next(err);
}
},
);
this.app.put(
'/api/alerts/:id',
async (req, res, next) => {
if (
!isValidAlertID(req.params.id)
|| (req.body.archived !== true && req.body.archived !== false)
) {
res.sendStatus(400);
return;
}
try {
const result = await this.pool.query(
'SELECT * FROM nsm_alerts WHERE alert_id=$1',
[
req.params.id,
],
);
if (result.rows.length === 0) {
res.sendStatus(404);
return;
}
if (result.rows.length !== 1) {
res.sendStatus(500);
return;
}
await this.pool.query(
'UPDATE nsm_alerts SET archived=$1 WHERE alert_id=$2',
[
req.body.archived,
req.params.id,
],
);
res.sendStatus(200);
} catch (err) {
next(err);
}
},
);
this.sendNotification = async () => {
try {
if (process.env.EMAIL_SNDR_ADDR && process.env.EMAIL_RCVR_ADDR) {
const currentEpochTimestamp = Math.floor(Date.now() / 1000.0);
const selectResult = await this.pool.query(
'SELECT alert_id, message FROM nsm_alerts '
+ 'WHERE notified=$1 '
+ 'ORDER BY alert_id DESC',
[
false,
],
);
let emailBody = '<ul>';
selectResult.rows.forEach(
(row) => {
emailBody += `<li><b>[${row.alert_id}]</b> ${row.message}</li>`;
},
);
if (emailBody !== '<ul>') {
emailBody += '</ul>';
await this.transporter.sendMail(
{
from: process.env.EMAIL_SNDR_ADDR,
to: process.env.EMAIL_RCVR_ADDR,
subject: `[HiveGuard] Notification ${currentEpochTimestamp}`,
html: emailBody,
},
);
const updatePromises = [];
selectResult.rows.forEach(
(row) => {
updatePromises.push(
new Promise(
(resolve, reject) => {
this.pool.query(
'UPDATE nsm_alerts SET notified=$1 WHERE alert_id=$2',
[
true,
row.alert_id,
],
(err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
},
);
},
),
);
},
);
if (updatePromises.length > 0) {
await Promise.all(updatePromises);
}
}
}
} catch (err) {
console.error(err);
}
};
this.shouldNotify = (previousTimestamp, potentialTimestamp) => {
if (!previousTimestamp) {
return true;
}
const newTime = parseFloat(potentialTimestamp);
const lastTime = parseFloat(previousTimestamp);
return (newTime > lastTime + this.notificationCooldown);
};
this.processPotentialAlerts = async (potentialAlerts) => {
try {
const selectPromises = [];
potentialAlerts.forEach(
(potentialAlert) => {
selectPromises.push(
new Promise(
(resolve, reject) => {
this.pool.query(
'SELECT MAX(epoch_timestamp) '
+ 'FROM nsm_alerts '
+ 'WHERE message=$1',
[
potentialAlert.message,
],
(err, selectResult) => {
if (err) {
reject(err);
} else {
resolve(
{
selectResult,
alertID: potentialAlert.alertID,
message: potentialAlert.message,
epochTimestamp: potentialAlert.epochTimestamp,
},
);
}
},
);
},
),
);
},
);
if (selectPromises.length > 0) {
const promiseResults = await Promise.all(selectPromises);
const insertPromises = [];
promiseResults.forEach(
(promiseResult) => {
if (
promiseResult.selectResult.rows.length === 1
&& this.shouldNotify(
promiseResult.selectResult.rows[0].max,
promiseResult.epochTimestamp,
)
) {
insertPromises.push(
new Promise(
(resolve, reject) => {
this.pool.query(
'INSERT INTO nsm_alerts (alert_id, message, '
+ 'epoch_timestamp, archived, notified) '
+ 'VALUES ($1, $2, $3, $4, $5)',
[
promiseResult.alertID,
promiseResult.message,
promiseResult.epochTimestamp,
false,
false,
],
(err, insertResult) => {
if (err) {
reject(err);
} else {
resolve(insertResult);
}
},
);
},
),
);
}
},
);
if (insertPromises.length > 0) {
await Promise.all(insertPromises);
this.sendNotification();
}
}
} catch (err) {
console.error(err);
}
};
this.inspectNWKAUXSeqnumsShortaddr = async (sensor, panid, shortaddr) => {
if (!(shortaddr in this.lastNWKAUXSeqnums[sensor][panid])) {
this.lastNWKAUXSeqnums[sensor][panid][shortaddr] = {
epochTimestamp: 0.0,
nwkauxSeqnum: 0,
};
}
const selectResult = await this.pool.query(
'SELECT epoch_timestamp, nwkaux_seqnum '
+ 'FROM wids_nwkaux_seqnums '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND srcpanid=$2 '
+ 'AND srcshortaddr=$3 '
+ 'AND epoch_timestamp>$4 '
+ 'ORDER BY epoch_timestamp',
[
sensor,
panid,
shortaddr,
this.lastNWKAUXSeqnums[sensor][panid][shortaddr].epochTimestamp,
],
);
let lastDecrease = null;
selectResult.rows.forEach(
(row) => {
if (
row.nwkaux_seqnum
< this.lastNWKAUXSeqnums[sensor][panid][shortaddr].nwkauxSeqnum
) {
lastDecrease = parseFloat(row.epoch_timestamp);
}
this.lastNWKAUXSeqnums[sensor][panid][shortaddr].epochTimestamp = (
parseFloat(row.epoch_timestamp)
);
this.lastNWKAUXSeqnums[sensor][panid][shortaddr].nwkauxSeqnum = (
row.nwkaux_seqnum
);
},
);
return {
sensor,
panid,
shortaddr,
lastDecrease,
};
};
this.inspectNWKAUXSeqnumsPANID = async (sensor, panid) => {
if (!(panid in this.lastNWKAUXSeqnums[sensor])) {
this.lastNWKAUXSeqnums[sensor][panid] = {};
}
const selectResult = await this.pool.query(
'SELECT DISTINCT srcshortaddr '
+ 'FROM wids_nwkaux_seqnums '
+ 'WHERE wids_sensor_id=$1 '
+ 'AND srcpanid=$2',
[
sensor,
panid,
],
);
const shortaddrPromises = [];
selectResult.rows.forEach(
(row) => {
shortaddrPromises.push(
this.inspectNWKAUXSeqnumsShortaddr(
sensor,
panid,
row.srcshortaddr,
),
);
},
);
const tmpFindings = [];
if (shortaddrPromises.length > 0) {
const shortaddrResults = await Promise.all(shortaddrPromises);
shortaddrResults.forEach(
(tmp) => {
if (tmp.lastDecrease) {
tmpFindings.push(
{
message: (
'A nearby device may be impersonating '
+ `the ${tmp.shortaddr} node of the ${tmp.panid} network`
),
epochTimestamp: tmp.lastDecrease,
},
);
}
},
);
}
return tmpFindings;
};
this.inspectNWKAUXSeqnumsSensor = async (sensor) => {
if (!(sensor in this.lastNWKAUXSeqnums)) {
this.lastNWKAUXSeqnums[sensor] = {};
}
const selectResult = await this.pool.query(
'SELECT DISTINCT srcpanid '
+ 'FROM wids_nwkaux_seqnums '
+ 'WHERE wids_sensor_id=$1',
[
sensor,
],
);
const panidPromises = [];
selectResult.rows.forEach(
(row) => {
panidPromises.push(
this.inspectNWKAUXSeqnumsPANID(
sensor,
row.srcpanid,
),
);
},
);
if (panidPromises.length > 0) {
const panidResults = await Promise.all(panidPromises);
return Array.prototype.concat.apply([], panidResults);
}
return [];
};
this.inspectNWKAUXSeqnums = async () => {
try {
const currentEpochTimestamp = Math.floor(Date.now() / 1000.0);
const selectResult = await this.pool.query(
'SELECT DISTINCT wids_sensor_id '
+ 'FROM wids_nwkaux_seqnums',
);
const sensorPromises = [];
selectResult.rows.forEach(
(row) => {
sensorPromises.push(
this.inspectNWKAUXSeqnumsSensor(
row.wids_sensor_id,
),
);
},
);
if (sensorPromises.length > 0) {
const sensorResults = await Promise.all(sensorPromises);
const newFindings = Array.prototype.concat.apply([], sensorResults);
const potentialAlerts = [];
const uniqueMessages = new Set();
for (let i = 0; i < newFindings.length; i += 1) {
if (!uniqueMessages.has(newFindings[i].message)) {
uniqueMessages.add(newFindings[i].message);
potentialAlerts.push(
{
alertID: `HG${currentEpochTimestamp}F${i}`,
message: newFindings[i].message,
epochTimestamp: newFindings[i].epochTimestamp,
},
);
}
}
if (potentialAlerts.length > 0) {
this.processPotentialAlerts(potentialAlerts);
}
}
} catch (err) {
console.error(err);
}
};
this.inspectNWKAUXSeqnumsPeriodically = () => {
this.inspectNWKAUXSeqnums();
setTimeout(this.inspectNWKAUXSeqnumsPeriodically, this.inspectionDelay);
};
this.inspectEvents = async () => {
try {
const currentEpochTimestamp = Math.floor(Date.now() / 1000.0);
const selectResult = await this.pool.query(
'SELECT row_id, wids_sensor_id, epoch_timestamp, description '
+ 'FROM wids_events '
+ 'WHERE inspected=$1 '
+ 'ORDER BY epoch_timestamp',
[
false,
],
);
const updatePromises = [];
for (let i = 0; i < selectResult.rows.length; i += 1) {
const alertID = `HG${currentEpochTimestamp}E${i}`;
const message = (
`The WIDS sensor with ID ${selectResult.rows[i].wids_sensor_id} `
+ `detected the following: ${selectResult.rows[i].description}`
);
const epochTimestamp = selectResult.rows[i].epoch_timestamp;
updatePromises.push(
new Promise(
(resolve, reject) => {
this.pool.query(
'UPDATE wids_events SET inspected=$1 WHERE row_id=$2',
[
true,
selectResult.rows[i].row_id,
],
(err, updateResult) => {
if (err) {
reject(err);
} else {
resolve(
{
updateResult,
alertID,
message,
epochTimestamp,
},
);
}
},
);
},
),
);
}
if (updatePromises.length > 0) {
const promiseResults = await Promise.all(updatePromises);
const potentialAlerts = [];
const uniqueMessages = new Set();
promiseResults.forEach(
(promiseResult) => {
if (!uniqueMessages.has(promiseResult.message)) {
uniqueMessages.add(promiseResult.message);
potentialAlerts.push(
{
alertID: promiseResult.alertID,
message: promiseResult.message,
epochTimestamp: promiseResult.epochTimestamp,
},
);
}
},
);
this.processPotentialAlerts(potentialAlerts);
}
} catch (err) {
console.error(err);
}
};
this.inspectEventsPeriodically = () => {
this.inspectEvents();
setTimeout(this.inspectEventsPeriodically, this.inspectionDelay);
};
this.startInspectionRoutine = async () => {
await this.sendNotification();
this.inspectEventsPeriodically();
this.inspectNWKAUXSeqnumsPeriodically();
};
}
start() {
this.startInspectionRoutine();
this.app.listen(
this.inspectionPortNumber,
this.inspectionIPAddress,
() => {
console.log(
`Started an inspection server at ${this.inspectionIPAddress}`
+ `:${this.inspectionPortNumber}`,
);
},
);
}
}
module.exports = InspectionServer;