mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-04 01:01:35 -07:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bc2de6e24 | ||
|
|
09b42166cc | ||
|
|
dbe490a042 | ||
|
|
5996e70f60 | ||
|
|
15366a7f2e | ||
|
|
d5d1684ef9 | ||
|
|
c1141fc9a8 | ||
|
|
d38dcda35b | ||
|
|
ac5224747e | ||
|
|
5c23bde21c | ||
|
|
8e83d9b67d | ||
|
|
30c004eb77 | ||
|
|
1b6dc94bae | ||
|
|
76d37edc63 |
17
.github/workflows/code-checks.yml
vendored
17
.github/workflows/code-checks.yml
vendored
@@ -17,6 +17,23 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🚨 Ensure DELETE FROM CurrentScan is not commented out
|
||||
run: |
|
||||
echo "🔍 Checking that DELETE FROM CurrentScan is not commented out..."
|
||||
|
||||
MATCHES=$(grep -RInE '^[[:space:]]*#[[:space:]]*db\.sql\.execute\("DELETE FROM CurrentScan"\)' \
|
||||
--include="*.py" .) || true
|
||||
|
||||
if [ -n "$MATCHES" ]; then
|
||||
echo "❌ Found commented-out DELETE FROM CurrentScan call:"
|
||||
echo "$MATCHES"
|
||||
echo
|
||||
echo "This line must NOT be commented out in committed code."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ DELETE FROM CurrentScan is active."
|
||||
fi
|
||||
|
||||
- name: Check for incorrect absolute '/php/' URLs in frontend code
|
||||
run: |
|
||||
echo "🔍 Checking for incorrect absolute '/php/' URLs (should be 'php/' or './php/')..."
|
||||
|
||||
6
.github/workflows/run-all-tests.yml
vendored
6
.github/workflows/run-all-tests.yml
vendored
@@ -3,8 +3,8 @@ name: 🧪 Manual Test Suite Selector
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_authoritative:
|
||||
description: '📂 authoritative_fields/ (Logic, Locks, IPs)'
|
||||
run_scan:
|
||||
description: '📂 scan/ (Scan, Logic, Locks, IPs)'
|
||||
type: boolean
|
||||
default: true
|
||||
run_api:
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
run: |
|
||||
PATHS=""
|
||||
# Folder Mapping with 'test/' prefix
|
||||
if [ "${{ github.event.inputs.run_authoritative }}" == "true" ]; then PATHS="$PATHS test/authoritative_fields/"; fi
|
||||
if [ "${{ github.event.inputs.scan }}" == "true" ]; then PATHS="$PATHS test/scan/"; fi
|
||||
if [ "${{ github.event.inputs.run_api }}" == "true" ]; then PATHS="$PATHS test/api_endpoints/ test/server/"; fi
|
||||
if [ "${{ github.event.inputs.run_backend }}" == "true" ]; then PATHS="$PATHS test/backend/"; fi
|
||||
if [ "${{ github.event.inputs.run_docker_env }}" == "true" ]; then PATHS="$PATHS test/docker_tests/"; fi
|
||||
|
||||
@@ -56,14 +56,14 @@ docker run -d \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
-e PORT=20211 \
|
||||
-e APP_CONF_OVERRIDE='{"GRAPHQL_PORT":"20214"}' \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
ghcr.io/netalertx/netalertx:latest
|
||||
```
|
||||
|
||||
Note: Your `/local_data_dir` should contain a `config` and `db` folder.
|
||||
|
||||
To deploy a containerized instance directly from the source repository, execute the following BASH sequence:
|
||||
```bash
|
||||
git clone https://github.com/jokob-sk/NetAlertX.git
|
||||
git clone https://github.com/netalertx/NetAlertX.git
|
||||
cd NetAlertX
|
||||
docker compose up --force-recreate --build
|
||||
# To customize: edit docker-compose.yaml and run that last command again
|
||||
|
||||
@@ -21,7 +21,7 @@ docker run \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
-e PORT=20211 \
|
||||
-e APP_CONF_OVERRIDE='{"GRAPHQL_PORT":"20214"}' \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
ghcr.io/netalertx/netalertx:latest
|
||||
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ Note: Your `/local_data_dir` should contain a `config` and `db` folder.
|
||||
|
||||
If possible, check if your issue got fixed in the `_dev` image before opening a new issue. The container is:
|
||||
|
||||
`ghcr.io/jokob-sk/netalertx-dev:latest`
|
||||
`ghcr.io/netalertx/netalertx-dev:latest`
|
||||
|
||||
> ⚠ Please backup your DB and config beforehand!
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ The following steps will guide you to set up your environment for local developm
|
||||
### 1. Download the code:
|
||||
|
||||
- `mkdir /development`
|
||||
- `cd /development && git clone https://github.com/jokob-sk/NetAlertX.git`
|
||||
- `cd /development && git clone https://github.com/netalertx/NetAlertX.git`
|
||||
|
||||
### 2. Create a DEV .env_dev file
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ services:
|
||||
netalertx:
|
||||
#use an environmental variable to set host networking mode if needed
|
||||
container_name: netalertx # The name when you docker contiainer ls
|
||||
image: ghcr.io/jokob-sk/netalertx:latest
|
||||
image: ghcr.io/netalertx/netalertx:latest
|
||||
network_mode: ${NETALERTX_NETWORK_MODE:-host} # Use host networking for ARP scanning and other services
|
||||
|
||||
read_only: true # Make the container filesystem read-only
|
||||
|
||||
@@ -31,7 +31,7 @@ docker run -d --rm --network=host \
|
||||
--tmpfs /tmp:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700 \
|
||||
-e PORT=20211 \
|
||||
-e APP_CONF_OVERRIDE={"GRAPHQL_PORT":"20214"} \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
ghcr.io/netalertx/netalertx:latest
|
||||
```
|
||||
|
||||
> Runtime UID/GID: The image defaults to a service user `netalertx` (UID/GID 20211). A separate readonly lock owner also uses UID/GID 20211 for 004/005 immutability. You can override the runtime UID/GID at build (ARG) or run (`--user` / compose `user:`) but must align writable mounts (`/data`, `/tmp*`) and tmpfs `uid/gid` to that choice.
|
||||
|
||||
@@ -35,9 +35,9 @@ services:
|
||||
netalertx:
|
||||
container_name: netalertx
|
||||
# Use this line for stable release
|
||||
image: "ghcr.io/jokob-sk/netalertx:latest"
|
||||
image: "ghcr.io/netalertx/netalertx:latest"
|
||||
# Or, use this for the latest development build
|
||||
# image: "ghcr.io/jokob-sk/netalertx-dev:latest"
|
||||
# image: "ghcr.io/netalertx/netalertx-dev:latest"
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
cap_drop: # Drop all capabilities for enhanced security
|
||||
|
||||
@@ -44,7 +44,7 @@ Use the following Compose snippet to deploy NetAlertX with a **static LAN IP** a
|
||||
```yaml
|
||||
services:
|
||||
netalertx:
|
||||
image: ghcr.io/jokob-sk/netalertx:latest
|
||||
image: ghcr.io/netalertx/netalertx:latest
|
||||
...
|
||||
networks:
|
||||
swarm-ipvlan:
|
||||
|
||||
@@ -32,12 +32,22 @@ NetAlertX is a lightweight, flexible platform for monitoring networks, tracking
|
||||

|
||||
|
||||
- **Real-Time Notifications**: Receive immediate alerts for new devices, disconnected devices, or unexpected changes.
|
||||
- **Customizable Triggers**: Define rules based on device type, IP ranges, presence, or other network parameters.
|
||||
- **Customizable Filters and Rules**: Define rules based on device type, IP ranges, presence, or other network parameters.
|
||||
- **Alert Deduplication & Suppression**: Avoid unnecessary noise with smart alert handling.
|
||||
- **Historical Logs**: Maintain a complete timeline of network events for review and reporting.
|
||||
|
||||
---
|
||||
|
||||
## Workflows for implementing Business rules
|
||||
|
||||

|
||||
|
||||
- **Custom rules**: Cretae custom flows and update device information based to scan results.
|
||||
- **Customizable Triggers**: Define rules based on any device data, including device type, IP ranges, presence, or other network parameters.
|
||||
- **Automated Updates**: Automate repetitive tasks, making network management more efficient.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Channel Notification
|
||||
|
||||

|
||||
|
||||
@@ -12,7 +12,7 @@ docker run --rm --network=host \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
-e PORT=20211 \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
ghcr.io/netalertx/netalertx:latest
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
@@ -70,7 +70,7 @@ If you use a custom `PUID` (e.g. `0`) and `GUID` (e.g. `100`) make sure you also
|
||||
docker run -it --rm --name netalertx --user "0" \
|
||||
-v /local_data_dir:/data \
|
||||
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
ghcr.io/netalertx/netalertx:latest
|
||||
```
|
||||
|
||||
2. Wait for logs showing **permissions being fixed**. The container will then **hang intentionally**.
|
||||
@@ -95,7 +95,7 @@ docker run -it --rm --name netalertx --user "0" \
|
||||
services:
|
||||
netalertx:
|
||||
container_name: netalertx
|
||||
image: "ghcr.io/jokob-sk/netalertx"
|
||||
image: "ghcr.io/netalertx/netalertx"
|
||||
network_mode: "host"
|
||||
cap_drop: # Drop all capabilities for enhanced security
|
||||
- ALL
|
||||
|
||||
@@ -318,7 +318,7 @@ As per user feedback, we’ve re-introduced the ability to control which user th
|
||||
services:
|
||||
netalertx:
|
||||
container_name: netalertx
|
||||
image: "ghcr.io/jokob-sk/netalertx"
|
||||
image: "ghcr.io/netalertx/netalertx"
|
||||
network_mode: "host"
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
@@ -80,9 +80,9 @@ services:
|
||||
netalertx:
|
||||
container_name: netalertx
|
||||
# Use this line for the stable release
|
||||
image: "ghcr.io/jokob-sk/netalertx:latest"
|
||||
image: "ghcr.io/netalertx/netalertx:latest"
|
||||
# Or use this line for the latest development build
|
||||
# image: "ghcr.io/jokob-sk/netalertx-dev:latest"
|
||||
# image: "ghcr.io/netalertx/netalertx-dev:latest"
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ You can specify the DNS server in the docker-compose to improve name resolution
|
||||
services:
|
||||
netalertx:
|
||||
container_name: netalertx
|
||||
image: "ghcr.io/jokob-sk/netalertx:latest"
|
||||
image: "ghcr.io/netalertx/netalertx:latest"
|
||||
...
|
||||
dns: # specifying the DNS servers used for the container
|
||||
- 10.8.0.1
|
||||
|
||||
@@ -37,8 +37,8 @@ services:
|
||||
netalertx:
|
||||
container_name: netalertx
|
||||
# use the below line if you want to test the latest dev image
|
||||
# image: "ghcr.io/jokob-sk/netalertx-dev:latest"
|
||||
image: "ghcr.io/jokob-sk/netalertx:latest"
|
||||
# image: "ghcr.io/netalertx/netalertx-dev:latest"
|
||||
image: "ghcr.io/netalertx/netalertx:latest"
|
||||
network_mode: "host"
|
||||
restart: unless-stopped
|
||||
cap_drop: # Drop all capabilities for enhanced security
|
||||
|
||||
@@ -538,6 +538,7 @@ body
|
||||
font-size: larger;
|
||||
float: right;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
hr
|
||||
@@ -929,6 +930,14 @@ height: 50px;
|
||||
.nav-tabs-custom .tab-content {
|
||||
overflow: scroll;
|
||||
}
|
||||
.infobox_label
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
.small-box .icon
|
||||
{
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.top_small_box_gray_text {
|
||||
|
||||
@@ -572,7 +572,7 @@ function purgeAllExecute() {
|
||||
data: JSON.stringify({
|
||||
dbtable: dbTable,
|
||||
columnName: 'Plugin',
|
||||
id: plugPrefix
|
||||
id: [plugPrefix]
|
||||
}),
|
||||
contentType: "application/json",
|
||||
success: function(response, textStatus) {
|
||||
@@ -603,15 +603,18 @@ function deleteListed(plugPrefixArg, dbTableArg) {
|
||||
|
||||
// Ask for confirmation
|
||||
showModalWarning(`${getString('Gen_Purge')} ${plugPrefix} ${dbTable}`, `${getString('Gen_AreYouSure')} (${idArr.length})`,
|
||||
`${getString('Gen_Cancel')}`, `${getString('Gen_Okay')}`, "deleteListedExecute");
|
||||
`${getString('Gen_Cancel')}`, `${getString('Gen_Okay')}`, () => deleteListedExecute(idArr));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
function deleteListedExecute() {
|
||||
function deleteListedExecute(idArr) {
|
||||
const apiBase = getApiBase();
|
||||
const apiToken = getSetting("API_TOKEN");
|
||||
const url = `${apiBase}/dbquery/delete`;
|
||||
|
||||
console.log(idArr);
|
||||
|
||||
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: url,
|
||||
@@ -619,7 +622,7 @@ function deleteListedExecute() {
|
||||
data: JSON.stringify({
|
||||
dbtable: dbTable,
|
||||
columnName: 'Index',
|
||||
id: idArr.toString()
|
||||
id: idArr
|
||||
}),
|
||||
contentType: "application/json",
|
||||
success: function(response, textStatus) {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
virtualisation.oci-containers = {
|
||||
containers = {
|
||||
netalertx = {
|
||||
image = "ghcr.io/jokob-sk/netalertx:${cfg.imageTag}";
|
||||
image = "ghcr.io/netalertx/netalertx:${cfg.imageTag}";
|
||||
autoStart = true;
|
||||
extraOptions = [
|
||||
"--network=host"
|
||||
|
||||
@@ -185,7 +185,7 @@ printf "%b\n" "${GREEN}[INSTALLING] ${RESET}Cloning app
|
||||
printf "%b\n" "--------------------------------------------------------------------------"
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
git clone https://github.com/jokob-sk/NetAlertX.git "$INSTALL_DIR/"
|
||||
git clone https://github.com/netalertx/NetAlertX.git "$INSTALL_DIR/"
|
||||
|
||||
if [ ! -f "$INSTALL_DIR/front/buildtimestamp.txt" ]; then
|
||||
date +%s > "$INSTALL_DIR/front/buildtimestamp.txt"
|
||||
|
||||
@@ -1287,14 +1287,22 @@ def dbquery_update(payload=None):
|
||||
def dbquery_delete(payload=None):
|
||||
data = request.get_json() or {}
|
||||
required = ["columnName", "id", "dbtable"]
|
||||
if not all(data.get(k) for k in required):
|
||||
return jsonify({"success": False, "message": "ERROR: Missing parameters", "error": "Missing required 'columnName', 'id', or 'dbtable' query parameter"}), 400
|
||||
if not all(k in data and data[k] for k in required):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": "ERROR: Missing parameters",
|
||||
"error": "Missing required 'columnName', 'id', or 'dbtable' query parameter"
|
||||
}), 400
|
||||
|
||||
return delete_query(
|
||||
column_name=data["columnName"],
|
||||
ids=data["id"],
|
||||
dbtable=data["dbtable"],
|
||||
)
|
||||
dbtable = data["dbtable"]
|
||||
column_name = data["columnName"]
|
||||
ids = data["id"]
|
||||
|
||||
# Ensure ids is a list
|
||||
if not isinstance(ids, list):
|
||||
ids = [ids]
|
||||
|
||||
return delete_query(column_name, ids, dbtable)
|
||||
|
||||
|
||||
# --------------------------
|
||||
|
||||
@@ -11,6 +11,7 @@ INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
|
||||
|
||||
from database import get_temp_db_connection # noqa: E402 [flake8 lint suppression]
|
||||
from logger import mylog # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
def read_query(raw_sql_b64):
|
||||
@@ -82,17 +83,18 @@ def delete_query(column_name, ids, dbtable):
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
if not isinstance(ids, list):
|
||||
ids = [ids]
|
||||
|
||||
deleted_count = 0
|
||||
for id_val in ids:
|
||||
sql = f"DELETE FROM {dbtable} WHERE {column_name} = ?"
|
||||
# Wrap table and column in quotes to handle reserved words
|
||||
sql = f'DELETE FROM "{dbtable}" WHERE "{column_name}" = ?'
|
||||
mylog("debug", f"[delete_query] sql {sql} with id={id_val}")
|
||||
cur.execute(sql, (id_val,))
|
||||
deleted_count += cur.rowcount
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({"success": True, "deleted_count": deleted_count})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ ALLOWED_NMAP_MODES = Literal[
|
||||
|
||||
NOTIFICATION_LEVELS = Literal["info", "warning", "error", "alert", "interrupt"]
|
||||
|
||||
ALLOWED_TABLES = Literal["Devices", "Events", "Sessions", "Settings", "CurrentScan", "Online_History", "Plugins_Objects"]
|
||||
ALLOWED_TABLES = Literal["Devices", "Events", "Sessions", "Settings", "CurrentScan", "Online_History", "Plugins_Objects", "Plugins_History"]
|
||||
|
||||
ALLOWED_LOG_FILES = Literal[
|
||||
"app.log", "app_front.log", "IP_changes.log", "stdout.log", "stderr.log",
|
||||
|
||||
@@ -103,7 +103,7 @@ def process_scan(db):
|
||||
|
||||
# Clear current scan as processed
|
||||
# 🐛 CurrentScan DEBUG: comment out below when debugging to keep the CurrentScan table after restarts/scan finishes
|
||||
# db.sql.execute("DELETE FROM CurrentScan")
|
||||
db.sql.execute("DELETE FROM CurrentScan")
|
||||
|
||||
# re-broadcast unread notifiation count to update FE
|
||||
update_unread_notifications_count()
|
||||
|
||||
@@ -41,6 +41,8 @@ def scan_db():
|
||||
devIsNew INTEGER DEFAULT 1,
|
||||
devFavorite INTEGER DEFAULT 0,
|
||||
devScan INTEGER DEFAULT 1,
|
||||
devAlertDown INTEGER DEFAULT 0,
|
||||
devAlertEvents INTEGER DEFAULT 1,
|
||||
|
||||
-- Authoritative Metadata Columns
|
||||
devMacSource TEXT,
|
||||
@@ -81,6 +83,41 @@ def scan_db():
|
||||
)
|
||||
""")
|
||||
|
||||
# 3. Events Table
|
||||
cur.execute("""
|
||||
CREATE TABLE Events (
|
||||
eve_MAC TEXT,
|
||||
eve_IP TEXT,
|
||||
eve_DateTime TEXT,
|
||||
eve_EventType TEXT,
|
||||
eve_AdditionalInfo TEXT,
|
||||
eve_PendingAlertEmail INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
# 4. LatestEventsPerMAC View
|
||||
cur.execute("""DROP VIEW IF EXISTS LatestEventsPerMAC;""")
|
||||
cur.execute("""
|
||||
CREATE VIEW LatestEventsPerMAC AS
|
||||
WITH RankedEvents AS (
|
||||
SELECT
|
||||
e.*,
|
||||
ROW_NUMBER() OVER (PARTITION BY e.eve_MAC ORDER BY e.eve_DateTime DESC) AS row_num
|
||||
FROM Events AS e
|
||||
)
|
||||
SELECT
|
||||
e.eve_MAC,
|
||||
e.eve_EventType,
|
||||
e.eve_DateTime,
|
||||
e.eve_PendingAlertEmail,
|
||||
d.devPresentLastScan,
|
||||
c.scanLastIP
|
||||
FROM RankedEvents AS e
|
||||
LEFT JOIN Devices AS d ON e.eve_MAC = d.devMac
|
||||
LEFT JOIN CurrentScan AS c ON e.eve_MAC = c.scanMac
|
||||
WHERE e.row_num = 1;
|
||||
""")
|
||||
|
||||
# 3. LatestDeviceScan View (Inner Join for Online Devices)
|
||||
cur.execute("""
|
||||
CREATE VIEW LatestDeviceScan AS
|
||||
@@ -104,4 +104,3 @@ def test_primary_ipv4_is_set_and_ipv6_preserved(scan_db, mock_device_handling):
|
||||
assert row["devLastIP"] == "10.0.0.5"
|
||||
assert row["devPrimaryIPv4"] == "10.0.0.5"
|
||||
assert row["devPrimaryIPv6"] == "2001:db8::2"
|
||||
|
||||
Reference in New Issue
Block a user