173 lines
5.8 KiB
Plaintext
173 lines
5.8 KiB
Plaintext
# 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"
|
|
|