From 264f05d50379730fcaea98f50d3d6ec379cb43f9 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 15 Dec 2025 06:13:59 +0300 Subject: [PATCH] first --- arpwatch.rsc | 111 +++++++++++++++++++++ arpwatch_with_bridge_hosts.rsc | 172 +++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 arpwatch.rsc create mode 100644 arpwatch_with_bridge_hosts.rsc diff --git a/arpwatch.rsc b/arpwatch.rsc new file mode 100644 index 0000000..0131c66 --- /dev/null +++ b/arpwatch.rsc @@ -0,0 +1,111 @@ +# ARPWATCH — simple & reliable version with per-MAC files in "arpwatch" folder +:local folder "arpwatch" +:local dbFile ($folder . "/arpwatch.csv") +:local sendTo "svetloe@koronovonoe-pivo.ru" +:local emailSubjectPrefix "ARPWatch" + +:local date [/system clock get date] +:local time [/system clock get time] +:local now ($date . " " . $time) + +:log info ("arpwatch: starting run at " . $now) + +# Ensure folder exists +:if ([:len [/file find name=$folder]] = 0) do={ + /file make-directory $folder + :log info ("arpwatch: created folder " . $folder) +} + +# Ensure CSV exists +:if ([:len [/file find name=$dbFile]] = 0) do={ + /file add name=$dbFile contents="mac,ip,first_seen,vendor" + :log info ("arpwatch: created database file " . $dbFile) +} + +# Read current CSV for in-memory append +:local oldCSV [/file get [/file find name=$dbFile] contents] + +# Buffers/counters +:local newRows "" +:local newCount 0 +:local arpCount 0 + +# Count ARP entries +:foreach id in=[/ip arp find] do={ :set arpCount ($arpCount + 1) } +:log info ("arpwatch: ARP entries found: " . $arpCount) + +# Iterate through ARP table +:foreach id in=[/ip arp find] do={ + :local mac [/ip arp get $id mac-address] + :local ip [/ip arp get $id address] + :local iface [/ip arp get $id interface] + + # Skip empty + :if ([:len $mac] = 0) do={ + :log info ("arpwatch: skipping, empty MAC (iface=" . $iface . " ip=" . $ip . ")") + } else={ + :if ([:len $ip] = 0) do={ + :log info ("arpwatch: skipping, empty IP for MAC " . $mac) + } else={ + + # Normalize MAC and filename + # :local mac_up [$mac] + :local macFileCore "" + :local i 0 + :local maclen [:len $mac] + :for i from=0 to=($maclen - 1) do={ + :local ch [:pick $mac $i ($i + 1)] + :if ($ch = ":") do={ :set macFileCore ($macFileCore . "-") } else={ :set macFileCore ($macFileCore . $ch) } + } + :local macFile ($folder . "/mac-" . $macFileCore) + + :log info ("arpwatch: checking MAC " . $mac . " at " . $ip . " on " . $iface) + + # Check if exists + :if ([:len [/file find name=$macFile]] > 0) do={ + :log info ("arpwatch: MAC exists, skip: " . $mac) + } else={ + :log info ("arpwatch: NEW MAC detected: " . $mac) + + :local row ($mac . "," . $ip . "," . $now . ",https://maclookup.app/search/result?mac=" . $mac) + + :if ([:len $newRows] = 0) do={ :set newRows $row } else={ :set newRows ($newRows . "\n" . $row) } + + :set oldCSV ($oldCSV . "\n" . $row) + + /file add name=$macFile contents=$now + + :set newCount ($newCount + 1) + } + } + } +} + +:log info ("arpwatch: new MAC count = " . $newCount) + +# Update CSV and send mail +:if ($newCount > 0) do={ + + :do { + /file set [/file find name=$dbFile] contents=$oldCSV + :log info ("arpwatch: database updated, appended " . $newCount . " new row(s)") + } on-error={ + :log error "arpwatch: failed to update CSV" + } + + :local subject ($emailSubjectPrefix . " - " . $newCount . " new MAC(s)") + :local body ("New MAC addresses detected at " . $now . ":\n\n" . $newRows . "\n\nDatabase: " . $dbFile) + + :do { + /tool e-mail send to=$sendTo subject=$subject body=$body + :log info ("arpwatch: e-mail sent to " . $sendTo) + } on-error={ + :log error "arpwatch: failed to send e-mail" + } + +} else={ + :log info "arpwatch: no new MACs found, nothing to update or send" +} + +:log info "arpwatch: run finished" + diff --git a/arpwatch_with_bridge_hosts.rsc b/arpwatch_with_bridge_hosts.rsc new file mode 100644 index 0000000..8e07666 --- /dev/null +++ b/arpwatch_with_bridge_hosts.rsc @@ -0,0 +1,172 @@ +# ARPWATCH — simple & reliable version with per-MAC files in "arpwatch" folder +:local folder "arpwatch" +:local dbFile ($folder . "/arpwatch.csv") +:local sendTo "svetloe@koronovonoe-pivo.ru" +:local emailSubjectPrefix "ARPWatch" + +:local date [/system clock get date] +:local time [/system clock get time] +:local now ($date . " " . $time) + +:log info ("arpwatch: starting run at " . $now) + +# Ensure folder exists +:if ([:len [/file find name=$folder]] = 0) do={ + /file make-directory $folder + :log info ("arpwatch: created folder " . $folder) +} + +# Ensure CSV exists +:if ([:len [/file find name=$dbFile]] = 0) do={ + /file add name=$dbFile contents="mac,ip,first_seen,vendor" + :log info ("arpwatch: created database file " . $dbFile) +} + +# Read current CSV for in-memory append +:local oldCSV [/file get [/file find name=$dbFile] contents] + +# Buffers/counters +:local newRows "" +:local newCount 0 +:local arpCount 0 + +# Count ARP entries +:foreach id in=[/ip arp find] do={ :set arpCount ($arpCount + 1) } +:log info ("arpwatch: ARP entries found: " . $arpCount) + +############################ +# Iterate through ARP table +############################ +:foreach id in=[/ip arp find] do={ + :local mac [/ip arp get $id mac-address] + :local ip [/ip arp get $id address] + :local iface [/ip arp get $id interface] + + # Skip empty + :if ([:len $mac] = 0) do={ + :log info ("arpwatch: skipping, empty MAC (iface=" . $iface . " ip=" . $ip . ")") + } else={ + :if ([:len $ip] = 0) do={ + :log info ("arpwatch: skipping, empty IP for MAC " . $mac) + } else={ + + # Normalize MAC and filename + :local macFileCore "" + :local i 0 + :local maclen [:len $mac] + :for i from=0 to=($maclen - 1) do={ + :local ch [:pick $mac $i ($i + 1)] + :if ($ch = ":") do={ :set macFileCore ($macFileCore . "-") } else={ :set macFileCore ($macFileCore . $ch) } + } + :local macFile ($folder . "/mac-" . $macFileCore) + + :log info ("arpwatch: checking MAC " . $mac . " at " . $ip . " on " . $iface) + + # Check if exists + :if ([:len [/file find name=$macFile]] > 0) do={ + :log info ("arpwatch: MAC exists, skip: " . $mac) + } else={ + :log info ("arpwatch: NEW MAC detected: " . $mac) + + :local row ($mac . "," . $ip . "," . $now . ",https://maclookup.app/search/result?mac=" . $mac) + + :if ([:len $newRows] = 0) do={ :set newRows $row } else={ :set newRows ($newRows . "\n" . $row) } + + :set oldCSV ($oldCSV . "\n" . $row) + + /file add name=$macFile contents=$now + + :set newCount ($newCount + 1) + } + } + } +} + +:log info ("arpwatch: new MAC count from ARP = " . $newCount) + +########################################## +# Also check bridge host table (bridge hosts) +# Use 'print as-value' and -> operator to safely access fields +########################################## +:local bridgeCount 0 +:foreach item in=[/interface bridge host print as-value] do={ + # access properties from associative array + :local bmac ($item->"mac-address") + :local biface ($item->"on-interface") + :local bvlan ($item->"vlan-id") + + :if ([:len $bmac] = 0) do={ + :log info ("arpwatch: bridge host entry with empty MAC, iface=" . $biface) + } else={ + + # Try to find IP for this MAC from ARP table (if present) + :local ip "" + :local arpMatch [/ip arp find where mac-address=$bmac] + :if ([:len $arpMatch] > 0) do={ + :set ip [/ip arp get $arpMatch address] + } else={ + :set ip "" + } + + # Normalize MAC and filename (same as above) + :local macFileCore "" + :local i 0 + :local maclen [:len $bmac] + :for i from=0 to=($maclen - 1) do={ + :local ch [:pick $bmac $i ($i + 1)] + :if ($ch = ":") do={ :set macFileCore ($macFileCore . "-") } else={ :set macFileCore ($macFileCore . $ch) } + } + :local macFile ($folder . "/mac-" . $macFileCore) + + :log info ("arpwatch: checking BRIDGE MAC " . $bmac . " on " . $biface . " vlan=" . $bvlan) + + # If per-MAC file already exists, skip (this prevents duplicates if ARP already handled it) + :if ([:len [/file find name=$macFile]] > 0) do={ + :log info ("arpwatch: BRIDGE MAC exists, skip: " . $bmac) + } else={ + :log info ("arpwatch: NEW BRIDGE MAC detected: " . $bmac) + + # Use IP if found, otherwise leave empty field + :local row ($bmac . "," . $ip . "," . $now . ",https://maclookup.app/search/result?mac=" . $bmac) + + :if ([:len $newRows] = 0) do={ :set newRows $row } else={ :set newRows ($newRows . "\n" . $row) } + + :set oldCSV ($oldCSV . "\n" . $row) + + /file add name=$macFile contents=$now + + :set newCount ($newCount + 1) + :set bridgeCount ($bridgeCount + 1) + } + } +} + +:log info ("arpwatch: new MAC count from BRIDGE = " . $bridgeCount) +:log info ("arpwatch: total new MAC count = " . $newCount) + +# Update CSV and send mail +:if ($newCount > 0) do={ + + :do { + /file set [/file find name=$dbFile] contents=$oldCSV + :log info ("arpwatch: database updated, appended " . $newCount . " new row(s)") + } on-error={ + :log error "arpwatch: failed to update CSV" + } + + :local subject ($emailSubjectPrefix . " - " . $newCount . " new MAC(s)") + :local body ("New MAC addresses detected at " . $now . ":\n\n" . $newRows . "\n\nDatabase: " . $dbFile) + + :do { + /tool e-mail send to=$sendTo subject=$subject body=$body + :log info ("arpwatch: e-mail sent to " . $sendTo) + } on-error={ + :log error "arpwatch: failed to send e-mail" + } + +} else={ + :log info "arpwatch: no new MACs found, nothing to update or send" +} + +:log info "arpwatch: run finished" +