42 Commits
Author SHA1 Message Date
Stone_Red e4447dc0be Update version 2026-01-22 21:15:15 +01:00
Stone_Red f332631696 Add monitoring flags, auto-discovery, and advanced filtering options 2026-01-22 20:59:48 +01:00
Stone_Red 3528f06ce4 Fix assembly version 2025-06-27 13:17:10 +02:00
Stone_Red e5f69fd256 Update version 2025-06-27 13:06:17 +02:00
Stone_Red a99903eec6 Update nuget packages 2025-06-27 13:03:38 +02:00
Stone_Red e6943924cc Allow files paths without directory as output paths 2025-06-27 13:01:16 +02:00
Stone_Red edc7ea95dd Update package manifest files 2024-10-30 20:49:50 +01:00
Stone_Red 4c38d52709 Add missing license in readme and small fixes 2024-10-30 19:00:44 +01:00
Stone_Red c094f0400e Update readme to new version and include banner 2024-10-29 12:30:23 +01:00
Stone_Red 975e7b7d59 Merge pull request #8 from Stone-Red-Code/main
Sync
2024-10-29 12:21:38 +01:00
Stone_Red 7e043c2fe2 Merge branch 'develop' into main 2024-10-29 12:21:27 +01:00
Stone_Red 6e86058862 Set "scan" as default 2024-10-29 12:17:14 +01:00
Stone_Red 1b5278bf09 Add monitoring mode and improve json output format 2024-10-27 01:17:10 +02:00
Stone_Red 701cd8e3fc Add command line arguments
- Save to JSON
- Save to CSV
- Retry
- Concurrency
- Silent
- Help
- Version
2024-10-25 14:45:26 +02:00
Stone_Red ae8055fac8 Add humanizer library 2024-10-25 00:18:05 +02:00
Stone_Red 11a45e1cc6 Add summary to package description 2024-06-21 15:29:57 +02:00
Stone_Red 08b84f3dad Update .nuspec 2024-06-04 20:24:21 +02:00
Stone_Red a06dfe82a8 Update readme 2024-06-04 17:56:02 +02:00
Stone_Red 3c5d165ebb Fix pluralization & alignment of output 2024-05-01 14:58:57 +02:00
Stone_Red 553fd97e2a Update arp-scanner.nuspec 2024-05-01 14:48:11 +02:00
Stone_Red 5ceee1092f Set Private to null instead of false 2024-05-01 02:00:12 +02:00
Stone_Red 9f49f2d595 Merge pull request #7 from Stone-Red-Code/develop
Fix snapcraft.yaml?
2024-05-01 01:51:24 +02:00
Stone_Red 92b1fd0598 Fix snapcraft.yaml? 2024-05-01 01:51:02 +02:00
Stone_Red 6f6a2c2ee5 Merge pull request #6 from Stone-Red-Code/develop
Update snapcraft architecture section
2024-05-01 01:46:29 +02:00
Stone_Red 687b882d5e Update snapcraft architecture section 2024-05-01 01:45:59 +02:00
Stone_Red 95774eaee8 Merge pull request #5 from Stone-Red-Code/develop
Update project to .NET 8 and auto update mac db
2024-05-01 01:37:05 +02:00
Stone_Red 6de6edbf48 Update project to .NET 8 and auto update mac db 2024-05-01 01:36:31 +02:00
Stone_Red dabc878624 Update choco VERIFICATION.txt 2024-02-08 07:58:49 +01:00
Stone_Red 1420655b8d Update snapcraft.yaml 2024-01-09 13:35:34 +01:00
Stone_Red 96b261238e Update choco package nuspec 2024-01-09 13:19:23 +01:00
Stone_Red 89f1401de9 Update README.md
Remove snap connect instructions because it now auto connects.
2024-01-05 14:28:50 +01:00
Stone_Red 924e0016cb Update twitter link in README.md to always show the latest updates 2024-01-03 12:23:41 +01:00
Stone_Red 40cb53d037 Update README.md 2024-01-03 12:22:28 +01:00
Stone_Red a0c2811fae Merge pull request #4 from Stone-Red-Code/develop
Develop
2024-01-03 11:58:46 +01:00
Stone_Red a4536f05d8 Debug build 2024-01-03 11:58:25 +01:00
Stone_Red 38ea2e99c7 Merge branch 'main' into develop 2024-01-03 00:43:04 +01:00
Stone_Red 3263d1bdae Remove unnecessary plug from snapcraft.yaml 2024-01-03 00:41:43 +01:00
Stone_Red ab327d9312 Merge pull request #3 from Stone-Red-Code/develop
Fix linux active detection issue
2024-01-03 00:06:48 +01:00
Stone_Red 9da39c3731 Update package version 2024-01-03 00:06:15 +01:00
Stone_Red a8eaa849f2 Fix active detection on linux 2024-01-03 00:00:24 +01:00
Stone_Red 8e1f610928 Add "Follow the development" section to readme 2024-01-02 16:41:20 +01:00
Stone_Red a1f5e7fe05 Use new GitHub markdown to highlight note 2024-01-02 16:05:01 +01:00
14 changed files with 783 additions and 44705 deletions
+20 -10
View File
@@ -1,22 +1,32 @@
# ARP-Scanner ![Arp-Scanner](https://socialify.git.ci/Stone-Red-Code/Arp-Scanner/image?description=1&forks=1&issues=1&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI%2FPgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDIwMDEwOTA0Ly9FTiIKICJodHRwOi8vd3d3LnczLm9yZy9UUi8yMDAxL1JFQy1TVkctMjAwMTA5MDQvRFREL3N2ZzEwLmR0ZCI%2BCjxzdmcgdmVyc2lvbj0iMS4wIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiB3aWR0aD0iMTAwLjAwMDAwMHB0IiBoZWlnaHQ9IjEwMC4wMDAwMDBwdCIgdmlld0JveD0iMCAwIDEwMC4wMDAwMDAgMTAwLjAwMDAwMCIKIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiPgo8bWV0YWRhdGE%2BCkNyZWF0ZWQgYnkgcG90cmFjZSAxLjEwLCB3cml0dGVuIGJ5IFBldGVyIFNlbGluZ2VyIDIwMDEtMjAxMQo8L21ldGFkYXRhPgogICA8cmVjdCB4PSIwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgcng9IjE1IiBmaWxsPSIjNWE1YTVhIiAvPgo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjAwMDAwMCw5NS4wMDAwMDApIHNjYWxlKDAuMTAwMDAwLC0wLjEwMDAwMCkiCmZpbGw9IiMwMDAwMDAiIHN0cm9rZT0ibm9uZSI%2BCjxwYXRoIGQ9Ik02NDAgOTIwIGMtOSAtMTcgLTMgLTIzIDQ3IC00OSBsNTggLTMwIC0xMDIgLTEgLTEwMyAwIDAgLTMwIGMwIC0yNQotNCAtMzAgLTI0IC0zMCAtMTQgMCAtMjggLTQgLTMxIC0xMCAtNCAtNiAtNTYgLTEwIC0xMjIgLTEwIC0xNDggMCAtMTg1IC0xMQotMjQ4IC03NSAtNzMgLTcyIC04OSAtMTM1IC02MCAtMjMzIDIwIC02NiA2NiAtMTE5IDEzMCAtMTQ4IDQ4IC0yMyA1OCAtMjQKMzE2IC0yNCAyNTkgMCAyNjcgLTEgMzEwIC0yMyAyNCAtMTMgNTcgLTQzIDc0IC02NiAyNSAtMzYgMzAgLTU0IDMzIC0xMTcgNAotNjUgNyAtNzQgMjMgLTc0IDE2IDAgMTkgOCAxOSA1NiAwIDMxIC01IDc1IC0xMiA5OCAtMTYgNTUgLTk5IDEzOCAtMTU0IDE1NAotMjggOCAtMTI2IDEyIC0zMDEgMTIgLTI1MyAwIC0yNjEgMSAtMzA0IDIzIC0yNCAxMyAtNTcgNDMgLTc0IDY2IC0yNiAzNyAtMzAKNTEgLTMwIDExMSAwIDYwIDQgNzQgMzAgMTExIDE3IDIzIDUwIDUzIDc0IDY2IDM5IDIwIDU4IDIzIDE2NyAyMyA3MSAwIDEyNQotNCAxMjkgLTEwIDMgLTUgMTcgLTEwIDMwIC0xMCAxOCAwIDI0IC02IDI3IC0yNyBsMyAtMjggMTk4IC0zIDE5NyAtMiAwIDEwMAowIDEwMCAtNTkgMCBjLTUzIDAgLTY5IDUgLTE0NSA0OSAtODEgNDcgLTg2IDQ5IC05NiAzMXogbTIxMCAtMTA5IGMzOSAtNSA0NQotMTAgNTMgLTM4IDUgLTE3IDYgLTQ4IDMgLTY4IGwtNyAtMzYgLTE1NiAyIC0xNTYgMSAtMzIgMzQgLTMyIDM0IDMzIDM1IGMzMQozMyAzNyAzNSA5NiAzNSAzNSAwIDcwIDUgNzggMTAgOCA2IDI5IDggNDUgNCAxNyAtNCA1MCAtOSA3NSAtMTN6Ii8%2BCjxwYXRoIGQ9Ik0zMjAgNjE5IGMtNTYgLTI1IC0xNTAgLTk3IC0xNTAgLTExNSAwIC0xNyA5MiAtODcgMTUwIC0xMTMgNjAgLTI3CjE1MCAtMjcgMjEwIDAgNTUgMjUgMTUwIDk3IDE1MCAxMTQgMCAxNyAtOTUgODkgLTE1MCAxMTQgLTYwIDI3IC0xNTAgMjcgLTIxMAoweiBtMTkyIC0yOSBjMjggLTM0IDIzIC04NCAtMTEgLTEyMiAtNDMgLTQ4IC0xMDkgLTQ4IC0xNTIgMCAtNDQgNDkgLTM3IDEyMQoxNCAxNDIgMTIgNiA0NiA4IDc2IDYgNDIgLTIgNTkgLTggNzMgLTI2eiBtLTIxMCAtNDkgYy00IC00NSAtMiAtNTIgMzEgLTg3CjMzIC0zNiAzOSAtMzkgOTIgLTM5IDUzIDAgNTkgMyA5MiAzOSAzMyAzNSAzNSA0MiAzMSA4NyBsLTUgNDkgMjYgLTE2IGMxNCAtOQozOCAtMjggNTQgLTQyIGwyNyAtMjYgLTI3IC0yOCBjLTE1IC0xNiAtNTQgLTQyIC04NyAtNTggLTgwIC00MSAtMTQyIC00MQotMjIyIDAgLTU0IDI3IC0xMjQgODQgLTExMyA5MyAxMyAxMiAxMDEgNzcgMTAzIDc3IDIgMCAxIC0yMiAtMiAtNDl6Ii8%2BCjxwYXRoIGQ9Ik0zODQgNTY1IGMtMTkgLTI5IC0xMiAtNTcgMjAgLTc0IDI4IC0xNiA2MSAtMiA3MSAyOSA5IDMwIC0yMCA3MAotNTAgNzAgLTE1IDAgLTMxIC0xMCAtNDEgLTI1eiIvPgo8cGF0aCBkPSJNMjc2IDIyMiBjLTMgLTUgLTE4IC00NSAtMzUgLTkwIC0yMSAtNTcgLTI2IC04MiAtMTcgLTgyIDcgMCAxOCAxMQoyNCAyNCAxNSAzNCA2OSAzMyA4NSAtMSA5IC0yMCA0NCAtMzUgMzUgLTE1IC0xIDQgLTE2IDQ1IC0zMyA5MCAtMjcgNzQgLTQ1Cjk3IC01OSA3NHogbTI4IC01NiBjMyAtMTMgOCAtMzEgMTEgLTM4IDUgLTExIC0xIC0xNCAtMjMgLTEwIC0yOSA0IC0yOSA1IC0xOQozOCAxMiA0MCAyMiA0MyAzMSAxMHoiLz4KPHBhdGggZD0iTTM5OSAyMjggYy0xIC0yIC0zIC00MiAtNCAtOTAgLTIgLTY5IDAgLTg4IDExIC04OCAxMCAwIDE0IDEzIDE0IDQxCjAgMzMgMyA0MCAxOCAzNyA5IC0yIDI1IC0yMCAzNSAtNDAgMTAgLTIxIDI1IC0zOCAzNCAtMzggMTIgMCA5IDEwIC0xMSA0NQotMjEgMzggLTIzIDQ3IC0xMSA1MiAyMiA4IDE5IDYwIC00IDczIC0xNyA5IC03OSAxNSAtODIgOHogbTcxIC0zMyBjMTggLTIxIDUKLTQ1IC0yNSAtNDUgLTIxIDAgLTI1IDUgLTI1IDMwIDAgMjMgNSAzMCAxOSAzMCAxMCAwIDI0IC03IDMxIC0xNXoiLz4KPHBhdGggZD0iTTU1MCAxNDAgYzAgLTUzIDQgLTkwIDEwIC05MCA2IDAgMTAgMTYgMTAgMzUgMCAzMiAyIDM1IDI5IDM1IDMwIDAKNjEgMzAgNjEgNTcgMCAyNSAtMzkgNTMgLTc1IDUzIGwtMzUgMCAwIC05MHogbTY0IDU3IGMyNiAtMTkgMTcgLTUxIC0xNyAtNTUKLTI1IC0zIC0yNyAtMSAtMjcgMzIgMCAzOSAxMyA0NSA0NCAyM3oiLz4KPC9nPgo8L3N2Zz4K&name=1&pattern=Signal&pulls=1&stargazers=1&theme=Auto)
> A lightweight cross-platform IP scanner
## Usage ## Usage
1. Download the latest release 1. Download & install the latest release
- Chocolatey (Windows): `choco install arp-scanner` - Chocolatey (Windows): `choco install arp-scanner`
- Snapcraft (Linux): `snap install arp-scanner`\ - Snapcraft (Linux): `snap install arp-scanner`
and `snap connect arp-scanner:network-control` to give it permission to send arp requests - GitHub (Windows/Linux): [releases](https://github.com/Stone-Red-Code/ARP-Scanner/releases)
- GitHub (Windows/Linux): https://github.com/Stone-Red-Code/ARP-Scanner/releases 1. Use the `arp-scanner` command
1. Start `arp-scanner` with the IP range it should scan as parameter (e.g. `192.168.1.0 - 192.168.1.255`) - `arp-scanner help` lists all available commands
- `arp-scanner scan` scans the network for devices
- `arp-scanner scan --help` lists all available options for the scan command
- `arp-scanner monitor` continuously scans the network for devices
- `arp-scanner monitor --help` lists all available options for the monitor command
> Note: This programm may not work on all linux distributions because the [ArpLookup](https://github.com/georg-jung/ArpLookup) library has some [limitations](https://github.com/georg-jung/ArpLookup#supported-platforms) > [!Note]
> This programm may not work on all linux distributions because the [ArpLookup](https://github.com/georg-jung/ArpLookup) library has some [limitations](https://github.com/georg-jung/ArpLookup#supported-platforms)
## Follow the development
[![Twitter(X)](http://img.shields.io/badge/Twitter-black.svg?&logo=x&style=for-the-badge&logoColor=white)](https://twitter.com/search?q=%23ArpScanner%20%40StoneRedCode&f=live)
## Example ## Example
Start `arp-scanner` with the IP range it should scan as parameter (e.g. `192.168.1.0-192.168.1.255`)
![ARP-Scanner-Example](https://user-images.githubusercontent.com/56473591/236293969-4e8a65d2-86a3-4f10-8837-2b1aa0490252.png) ![ARP-Scanner-Example](https://user-images.githubusercontent.com/56473591/236293969-4e8a65d2-86a3-4f10-8837-2b1aa0490252.png)
## Third party licenses ## Third party licenses
- [ArpLookup](https://github.com/georg-jung/ArpLookup) - [MIT](https://github.com/georg-jung/ArpLookup/blob/master/LICENSE.txt) - [ArpLookup](https://github.com/georg-jung/ArpLookup) - [MIT](https://github.com/georg-jung/ArpLookup/blob/master/LICENSE.txt)
- [IPAddressRange](https://github.com/jsakamoto/ipaddressrange) - [MPL-2.0](https://github.com/jsakamoto/ipaddressrange/blob/master/LICENSE) - [IPAddressRange](https://github.com/jsakamoto/ipaddressrange) - [MPL-2.0](https://github.com/jsakamoto/ipaddressrange/blob/master/LICENSE)
- [Humanizer](https://github.com/Humanizr/Humanizer) - [MIT](https://github.com/Humanizr/Humanizer/blob/main/license.txt)
+17 -12
View File
@@ -40,7 +40,7 @@ enclosed in quotation marks, you should use an editor that supports UTF-8, not t
<!-- Note that unstable versions like 0.0.1 can be considered a released version, but it's <!-- Note that unstable versions like 0.0.1 can be considered a released version, but it's
possible that one can release a 0.0.1-beta before you release a 0.0.1 version. If the version possible that one can release a 0.0.1-beta before you release a 0.0.1 version. If the version
number is final, that is considered a released version and not a prerelease. --> number is final, that is considered a released version and not a prerelease. -->
<version>0.2.0.0</version> <version>1.2.0.0</version>
<!-- <packageSourceUrl>Where is this Chocolatey package located (think GitHub)? packageSourceUrl <!-- <packageSourceUrl>Where is this Chocolatey package located (think GitHub)? packageSourceUrl
is highly recommended for the community feed</packageSourceUrl>--> is highly recommended for the community feed</packageSourceUrl>-->
<!-- owners is a poor name for maintainers of the package. It sticks around by this name for <!-- owners is a poor name for maintainers of the package. It sticks around by this name for
@@ -67,26 +67,31 @@ enclosed in quotation marks, you should use an editor that supports UTF-8, not t
<!--<docsUrl>At <!--<docsUrl>At
what url are the software docs located?</docsUrl>--> what url are the software docs located?</docsUrl>-->
<!--<mailingListUrl></mailingListUrl>--> <!--<mailingListUrl></mailingListUrl>-->
<!--<bugTrackerUrl></bugTrackerUrl>--> <bugTrackerUrl>https://github.com/Stone-Red-Code/ARP-Scanner/issues</bugTrackerUrl>
<tags>arp-scanner arp p2p fiel-transfer</tags> <tags>arp-scanner arp</tags>
<summary>A lightweight cross-platform IP scanner</summary> <summary>A lightweight cross-platform IP scanner</summary>
<description> <description>
**Usage** **Usage**
1. Download the latest release 1. Download &amp; install the latest release
- Chocolatey (Windows): `choco install arp-scanner --version=0.2.0` - Chocolatey (Windows): `choco install arp-scanner`
- Snapcraft (Linux): `snap install arp-scanner`\ - Snapcraft (Linux): `snap install arp-scanner`
and `snap connect arp-scanner:network-control` to give it permission to send arp requests - GitHub (Windows/Linux): releases (https://github.com/Stone-Red-Code/ARP-Scanner/releases)
- GitHub (Windows/Linux): https://github.com/Stone-Red-Code/ARP-Scanner/releases 2. Use the `arp-scanner` command
1. Start `arp-scanner` with the IP range it should scan as parameter (e.g. `192.168.1.0 - 192.168.1.255`) - `arp-scanner help` lists all available commands
- `arp-scanner scan` scans the network for devices
- `arp-scanner scan --help` lists all available options for the scan command
- `arp-scanner monitor` continuously scans the network for devices
- `arp-scanner monitor --help` lists all available options for the monitor command
&gt; Note: This programm may not work on all linux distributions because the [ArpLookup](https://github.com/georg-jung/ArpLookup) library has some [limitations](https://github.com/georg-jung/ArpLookup#supported-platforms) Note: This programm may not work on all linux distributions because the ArpLookup (https://github.com/georg-jung/ArpLookup) library has some limitations (https://github.com/georg-jung/ArpLookup#supported-platforms)
**Third party licenses** **Third party licenses**
- ArpLookup (https://github.com/georg-jung/ArpLookup) - MIT (https://github.com/georg-jung/ArpLookup/blob/master/LICENSE.txt) - ArpLookup (https://github.com/georg-jung/ArpLookup) - MIT (https://github.com/georg-jung/ArpLookup/blob/master/LICENSE.txt)
- IPAddressRange (https://github.com/jsakamoto/ipaddressrange) - MPL-2.0 (https://github.com/jsakamoto/ipaddressrange/blob/master/LICENSE) - IPAddressRange (https://github.com/jsakamoto/ipaddressrange) - MPL-2.0 (https://github.com/jsakamoto/ipaddressrange/blob/master/LICENSE)
- Humanizer (https://github.com/Humanizr/Humanizer) - MIT (https://github.com/Humanizr/Humanizer/blob/main/license.txt)
</description> </description>
<!-- <releaseNotes>__REPLACE_OR_REMOVE__MarkDown_Okay</releaseNotes> --> <releaseNotes>https://github.com/Stone-Red-Code/ARP-Scanner/releases/latest</releaseNotes>
<!-- =============================== --> <!-- =============================== -->
<!-- Specifying dependencies and version ranges? <!-- Specifying dependencies and version ranges?
@@ -112,6 +117,6 @@ enclosed in quotation marks, you should use an editor that supports UTF-8, not t
<files> <files>
<!-- this section controls what actually gets packaged into the Chocolatey package --> <!-- this section controls what actually gets packaged into the Chocolatey package -->
<file src="tools\**" target="tools" /> <file src="tools\**" target="tools" />
<file src="..\src\ARP-Scanner\bin\Release\net6.0\**" target="tools" /> <file src="..\src\ARP-Scanner\bin\Release\net8.0\**" target="tools" />
</files> </files>
</package> </package>
+4 -2
View File
@@ -2,5 +2,7 @@
Verification is intended to assist the Chocolatey moderators and community Verification is intended to assist the Chocolatey moderators and community
in verifying that this package's contents are trustworthy. in verifying that this package's contents are trustworthy.
Checksum: c37a793bae228da768c9d4223d12608bfa28decbe7142e9ac19a49795cf23b82b2f0434032b98883343c658276bf2e4423926fbf79c65c48ae6cf11ad48d37ad Source Code: https://github.com/Stone-Red-Code/Arp-Scanner
Veify using: certutil -hashfile ARP-Scanner.exe sha512 Releases: https://github.com/Stone-Red-Code/ARP-Scanner/releases
Checksum: 551d33306839687dcb226e5290e4bb4d3d5786a9231799e0dba9f9c2a3b4711c346bf2aeb29f4fab54008b6b19c88ebb2649c6545343d41b11bed6b034407374
Verify using: certutil -hashfile C:\ProgramData\Chocolatey\lib\arp-scanner\ARP-Scanner.exe sha512
+16 -13
View File
@@ -1,25 +1,28 @@
name: arp-scanner # you probably want to 'snapcraft register <name>' name: arp-scanner # you probably want to 'snapcraft register <name>'
version: "0.2.0.0" # just for humans, typically '1.2+git' or '1.3.2' version: "1.2.0.0" # just for humans, typically '1.2+git' or '1.3.2'
grade: stable # must be 'stable' to release into candidate/stable channels grade: stable # must be 'stable' to release into candidate/stable channels
summary: A lightweight cross-platform IP scanner # 79 char long summary summary: A lightweight cross-platform IP scanner # 79 char long summary
description: | description: |
**Usage** **Usage**
1. Download the latest release 1. Download & install the latest release
- Chocolatey (Windows): `choco install arp-scanner --version=0.2.0` - Chocolatey (Windows): `choco install arp-scanner`
- Snapcraft (Linux): `snap install arp-scanner`\ - Snapcraft (Linux): `snap install arp-scanner`
and `snap connect arp-scanner:network-control` to give it permission to send arp requests - GitHub (Windows/Linux): releases (https://github.com/Stone-Red-Code/ARP-Scanner/releases)
- GitHub (Windows/Linux): https://github.com/Stone-Red-Code/ARP-Scanner/releases 2. Use the `arp-scanner` command
1. Start `arp-scanner` with the IP range it should scan as parameter (e.g. `192.168.1.0 - 192.168.1.255`) - `arp-scanner help` lists all available commands
- `arp-scanner scan` scans the network for devices
- `arp-scanner scan --help` lists all available options for the scan command
- `arp-scanner monitor` continuously scans the network for devices
- `arp-scanner monitor --help` lists all available options for the monitor command
> Note: This programm may not work on all linux distributions because the [ArpLookup](https://github.com/georg-jung/ArpLookup) library has some [limitations](https://github.com/georg-jung/ArpLookup#supported-platforms) > Note: This programm may not work on all linux distributions because the ArpLookup (https://github.com/georg-jung/ArpLookup) library has some limitations (https://github.com/georg-jung/ArpLookup#supported-platforms)
**Third party licenses** **Third party licenses**
- ArpLookup (https://github.com/georg-jung/ArpLookup) - MIT (https://github.com/georg-jung/ArpLookup/blob/master/LICENSE.txt) - ArpLookup (https://github.com/georg-jung/ArpLookup) - MIT (https://github.com/georg-jung/ArpLookup/blob/master/LICENSE.txt)
- IPAddressRange (https://github.com/jsakamoto/ipaddressrange) - MPL-2.0 (https://github.com/jsakamoto/ipaddressrange/blob/master/LICENSE) - IPAddressRange (https://github.com/jsakamoto/ipaddressrange) - MPL-2.0 (https://github.com/jsakamoto/ipaddressrange/blob/master/LICENSE)
- Humanizer (https://github.com/Humanizr/Humanizer) - MIT (https://github.com/Humanizr/Humanizer/blob/main/license.txt)
base: core22 # the base snap is the execution environment for this snap base: core22 # the base snap is the execution environment for this snap
architectures: architectures:
- build-on: amd64 - build-on: amd64
- build-on: arm64 - build-on: arm64
@@ -34,7 +37,7 @@ parts:
dotnet-self-contained-runtime-identifier: linux-x64 dotnet-self-contained-runtime-identifier: linux-x64
source: ./src/ARP-Scanner source: ./src/ARP-Scanner
build-packages: build-packages:
- dotnet-sdk-6.0 - dotnet-sdk-8.0
stage-packages: stage-packages:
- libicu70 - libicu70
@@ -42,5 +45,5 @@ apps:
arp-scanner: arp-scanner:
command: ARP-Scanner command: ARP-Scanner
plugs: plugs:
- network - network-observe
- network-control - home
+8 -9
View File
@@ -2,22 +2,21 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RootNamespace>ARP_Scanner</RootNamespace> <RootNamespace>ARP_Scanner</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>1.2.0.0</Version>
<AssemblyVersion></AssemblyVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ArpLookup" Version="2.0.3" /> <PackageReference Include="ArpLookup" Version="2.0.3" />
<PackageReference Include="IPAddressRange" Version="6.0.0" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Stone_Red-C-Sharp-Utilities" Version="1.0.3.1" /> <PackageReference Include="CuteUtils" Version="1.1.1" />
</ItemGroup> <PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="IPAddressRange" Version="6.2.0" />
<ItemGroup>
<None Update="mac-vendors.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>
+53
View File
@@ -0,0 +1,53 @@
namespace ARP_Scanner;
internal class JsonResult
{
public IEnumerable<HostInformation> Hosts { get; set; } = [];
public IEnumerable<HostInformation> NewHosts { get; set; } = [];
public IEnumerable<HostInformation> RemovedHosts { get; set; } = [];
public static JsonResult Parse(IEnumerable<string[]> activeHosts, List<string[]> previousHosts)
{
JsonResult result = new JsonResult
{
Hosts = activeHosts.Select(HostInformation.Parse)
};
if (previousHosts.Count == 0)
{
return result;
}
result.NewHosts = result.Hosts.Where(host => !previousHosts.Exists(previousHost => previousHost[1] == host.Mac));
result.RemovedHosts = previousHosts.Where(previousHost => !result.Hosts.Any(host => host.Mac == previousHost[1])).Select(HostInformation.Parse);
return result;
}
public class HostInformation
{
public required string Ip { get; set; }
public required string Mac { get; set; }
public required string VendorName { get; set; }
public required string BlockType { get; set; }
public bool? Private { get; set; }
public required string LastUpdate { get; set; }
public static HostInformation Parse(string[] row)
{
bool success = bool.TryParse(row[4], out bool isPrivate);
return new HostInformation
{
Ip = row[0],
Mac = row[1],
VendorName = row[2],
BlockType = row[3],
Private = success ? isPrivate : null,
LastUpdate = row[5]
};
}
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace ARP_Scanner;
internal class MacDatabase
{
public DateTime LastUpdate { get; set; }
public List<MacInformation> MacInformations { get; set; } = [];
}
+21
View File
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace ARP_Scanner;
internal class MacInformation
{
[JsonPropertyName("macPrefix")]
public required string MacPrefix { get; set; }
[JsonPropertyName("vendorName")]
public required string VendorName { get; set; }
[JsonPropertyName("private")]
public bool? Private { get; set; }
[JsonPropertyName("blockType")]
public required string BlockType { get; set; }
[JsonPropertyName("lastUpdate")]
public required string LastUpdate { get; set; }
}
+93 -29
View File
@@ -1,57 +1,121 @@
using System.Text.RegularExpressions; using CuteUtils.Misc;
using System.Net.Http.Json;
using System.Text.Json;
namespace ARP_Scanner; namespace ARP_Scanner;
internal class MacVendorLookup internal partial class MacVendorLookup
{ {
public readonly string[] header; private const string macLookupUrl = "https://maclookup.app/downloads/json-database/get-db";
private readonly string[][] fields; private readonly HttpClient httpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(20) };
public MacVendorLookup(string csvPath) private MacDatabase macDatabase = new MacDatabase();
public async Task Initialize(bool silent)
{ {
if (!File.Exists(csvPath)) if (macDatabase.MacInformations.Count != 0)
{ {
header = Array.Empty<string>(); // Already initialized
fields = Array.Empty<string[]>();
return; return;
} }
string[] lines = File.ReadAllLines(csvPath); string cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "macDatabase.json");
header = lines[0].Split(','); // Snap support
fields = lines.Skip(1).Select(l => Regex.Split(l, ",(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))")).ToArray(); string? snapUserCommon = Environment.GetEnvironmentVariable("SNAP_USER_COMMON");
if (snapUserCommon is not null)
{
cachePath = Path.Combine(snapUserCommon, "macDatabase.json");
} }
public string[] GetInformation(string macAdress) if (File.Exists(cachePath))
{ {
string[]? data = Array.Find(fields, f => macAdress.StartsWith(f[0])); try
{
macDatabase = JsonSerializer.Deserialize<MacDatabase>(File.ReadAllText(cachePath)) ?? new MacDatabase();
}
catch (Exception ex)
{
if (!silent)
{
ConsoleExt.WriteLine($"Failed to read MAC database cache: {ex.Message}", ConsoleColor.Red);
}
}
}
if (fields.Length == 0 || header.Length == 0) // Update MAC database if it's older than a week
if (macDatabase.LastUpdate > DateTime.Now.AddDays(-7))
{ {
return Array.Empty<string>(); if (!silent)
{
ConsoleExt.WriteLine("Using cached MAC database...", ConsoleColor.DarkYellow);
} }
else if (data is null) return;
{
string[] result = new string[header.Length - 1];
for (int i = 0; i < result.Length; i++)
{
result[i] = "Unknown";
} }
return result;
if (!silent)
{
ConsoleExt.WriteLine("Downloading MAC database from maclookup.app...", ConsoleColor.DarkYellow);
}
List<MacInformation>? newMacInformation = null;
try
{
newMacInformation = await httpClient.GetFromJsonAsync<List<MacInformation>>(macLookupUrl);
}
catch (Exception ex)
{
if (!silent)
{
ConsoleExt.WriteLine($"Failed to download MAC database: {ex.Message}", ConsoleColor.Red);
}
}
if (newMacInformation is null)
{
if (silent)
{
return;
}
if (macDatabase.MacInformations.Count != 0)
{
ConsoleExt.WriteLine("Failed to download MAC database, using cache...", ConsoleColor.DarkYellow);
} }
else else
{ {
List<string> result = new List<string>(); ConsoleExt.WriteLine("Failed to download MAC database and no cache found, using empty database...", ConsoleColor.Red);
for (int i = 1; i < header.Length; i++)
{
result.Add($"{data[i]}");
} }
return result.ToArray(); }
else
{
if (!silent)
{
ConsoleExt.WriteLine("MAC database downloaded successfully!", ConsoleColor.Green);
}
_ = Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
macDatabase.MacInformations = newMacInformation;
macDatabase.LastUpdate = DateTime.Now;
File.WriteAllText(cachePath, JsonSerializer.Serialize(macDatabase));
} }
} }
public string[] GetHeader() public MacInformation GetInformation(string macAddress)
{ {
return header!.Skip(1).ToArray(); macAddress = macAddress.Replace("-", ":")[..8].ToUpper();
return macDatabase.MacInformations.Find(m => m.MacPrefix == macAddress) ?? new MacInformation()
{
MacPrefix = macAddress,
VendorName = "Unknown",
BlockType = "Unknown",
Private = null,
LastUpdate = "Unknown"
};
} }
} }
+22
View File
@@ -0,0 +1,22 @@
using CommandLine;
namespace ARP_Scanner;
[Verb("monitor", HelpText = "Continuously monitor the specified IP range.")]
internal class MonitorOptions : ScanOptions
{
[Option('d', "delay", Required = false, Default = 60, HelpText = "The delay between each scan in seconds.")]
public int Delay { get; set; }
[Option("changes-only", Required = false, HelpText = "During monitoring, only print when hosts are added or removed.")]
public bool ChangesOnly { get; set; }
[Option("summary", Required = false, HelpText = "Print a compact summary after each scan instead of per-IP progress.")]
public bool Summary { get; set; }
[Option("on-new", Required = false, HelpText = "Command to execute when new hosts appear. Receives JSON on stdin.")]
public string? OnNewCommand { get; set; }
[Option("on-removed", Required = false, HelpText = "Command to execute when hosts disappear. Receives JSON on stdin.")]
public string? OnRemovedCommand { get; set; }
}
+452 -27
View File
@@ -1,94 +1,519 @@
using ArpLookup; using ArpLookup;
using CommandLine;
using CuteUtils.Misc;
using Humanizer;
using Humanizer.Localisation;
using NetTools; using NetTools;
using Stone_Red_Utilities.CollectionExtentions;
using Stone_Red_Utilities.ConsoleExtentions;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net; using System.Net;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ARP_Scanner; namespace ARP_Scanner;
internal static class Program internal static class Program
{ {
private static async Task Main(string[] args) private static readonly MacVendorLookup macVendorLookup = new();
{ private static readonly List<string[]> previouslyActiveHosts = [];
MacVendorLookup macVendorLookup = new MacVendorLookup("mac-vendors.csv");
private static readonly JsonSerializerOptions jsonSerializerOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private static async Task<int> Main(string[] args)
{
if (!Arp.IsSupported) if (!Arp.IsSupported)
{ {
ConsoleExt.WriteLine("ARP is not supported on this platform!", ConsoleColor.Red); ConsoleExt.WriteLine("ARP is not supported on this platform!", ConsoleColor.Red);
return; return 1;
} }
if (!IPAddressRange.TryParse(string.Join("", args), out IPAddressRange ipAddressRange)) return await Parser.Default.ParseArguments<ScanOptions, MonitorOptions>(args)
.MapResult(
(MonitorOptions monitorOptions) => StartMonitor(monitorOptions),
(ScanOptions scanOptions) => StartScan(scanOptions),
HandleParseError);
}
private static async Task<int> StartMonitor(MonitorOptions options)
{
string monitorTarget = string.IsNullOrWhiteSpace(options.IpRange) && options.AutoDiscover
? "all local subnets"
: options.IpRange ?? "<none>";
ConsoleExt.WriteLine($"Monitoring IP range '{monitorTarget}' every {options.Delay} seconds...", ConsoleColor.DarkYellow);
while (true)
{
int exitCode = await StartScan(options);
if (exitCode != 0)
{
return exitCode;
}
Console.WriteLine();
Stopwatch stopwatch = Stopwatch.StartNew();
bool printed = false;
while (stopwatch.Elapsed < TimeSpan.FromSeconds(options.Delay))
{
TimeSpan remainingTime = TimeSpan.FromSeconds(options.Delay) - stopwatch.Elapsed;
Console.Write($"\rNext scan in: {remainingTime.Humanize(3, minUnit: TimeUnit.Second)} ");
await Task.Delay(1000);
printed = true;
}
if (printed)
{
Console.Write($"\r{new string(' ', Console.WindowWidth)}\r");
}
}
}
private static async Task<int> StartScan(ScanOptions options)
{
IEnumerable<IPAddress> ipAddresses;
if (options.AutoDiscover && string.IsNullOrWhiteSpace(options.IpRange))
{
ipAddresses = GetLocalSubnets();
if (!ipAddresses.Any())
{
ConsoleExt.WriteLine("No local IPv4 subnets found for auto discovery.", ConsoleColor.Red);
return 2;
}
}
else
{
if (string.IsNullOrWhiteSpace(options.IpRange))
{
ConsoleExt.WriteLine("IP range is required when auto discovery is disabled.", ConsoleColor.Red);
return 2;
}
if (!IPAddressRange.TryParse(options.IpRange, out IPAddressRange ipAddressRange))
{ {
ConsoleExt.WriteLine("Invalid IP range!", ConsoleColor.Red); ConsoleExt.WriteLine("Invalid IP range!", ConsoleColor.Red);
return; return 2;
} }
long ipAddressesCount = ipAddressRange.Count(); ipAddresses = ipAddressRange;
}
await macVendorLookup.Initialize(options.Silent);
long ipAddressesCount = ipAddresses is IPAddressRange range
? range.Count()
: ipAddresses.LongCount();
long processedIpAddressesCount = 0; long processedIpAddressesCount = 0;
int numberOfDigits = ipAddressesCount.ToString().Length; int numberOfDigits = ipAddressesCount.ToString().Length;
int exitCode = 0;
List<string> header = new List<string>() { "IP", "MAC" }; ConcurrentBag<string[]> activeHosts = [];
ConcurrentBag<string[]> activeHosts = new ConcurrentBag<string[]>();
header.AddRange(macVendorLookup.GetHeader()); // If you want to change the header, you also need to change the JsonResult class
List<string> header = [
"IP",
"MAC",
nameof(MacInformation.VendorName).Humanize(LetterCasing.Title),
nameof(MacInformation.BlockType).Humanize(LetterCasing.Title),
nameof(MacInformation.Private).Humanize(LetterCasing.Title),
nameof(MacInformation.LastUpdate).Humanize(LetterCasing.Title)];
if (!options.Silent)
{
ConsoleExt.WriteLine("Starting scan...", ConsoleColor.DarkYellow); ConsoleExt.WriteLine("Starting scan...", ConsoleColor.DarkYellow);
}
await Parallel.ForEachAsync(ipAddressRange, async (IPAddress ipAddress, CancellationToken _) => ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = options.Concurrency
};
bool suppressPerIpOutput = options is MonitorOptions monitorOptions && (monitorOptions.ChangesOnly || monitorOptions.Summary);
await Parallel.ForEachAsync(ipAddresses, parallelOptions, async (IPAddress ipAddress, CancellationToken _) =>
{ {
PhysicalAddress? mac = null; PhysicalAddress? mac = null;
bool fail = false; bool fail = false;
int retry = options.Retry;
do
{
try try
{ {
mac = await Arp.LookupAsync(ipAddress); mac = await Arp.LookupAsync(ipAddress);
fail = false;
} }
catch catch (Exception ex)
{ {
if (!options.Silent)
{
ConsoleExt.WriteLine($"Failed to lookup MAC address for {ipAddress}: {ex.Message}", ConsoleColor.Red);
}
fail = true; fail = true;
} }
}
while (retry-- > 0 && (mac is null || fail));
long localProcessedIpAddressesCount = Interlocked.Increment(ref processedIpAddressesCount); long localProcessedIpAddressesCount = Interlocked.Increment(ref processedIpAddressesCount);
if (mac is not null) if (mac is not null && Array.Exists(mac.GetAddressBytes(), b => b != 0))
{ {
string formattedMac = BitConverter.ToString(mac.GetAddressBytes()); string formattedMac = BitConverter.ToString(mac.GetAddressBytes());
List<string> info = new List<string> { ipAddress.ToString(), formattedMac }; MacInformation macInformation = macVendorLookup.GetInformation(formattedMac);
info.AddRange(macVendorLookup.GetInformation(formattedMac));
List<string> info = [ipAddress.ToString(), formattedMac, macInformation.VendorName, macInformation.BlockType, macInformation.Private?.ToString() ?? "Unknown", macInformation.LastUpdate];
if (!options.Silent && !suppressPerIpOutput)
{
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Active: {ipAddress}", ConsoleColor.Green); ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Active: {ipAddress}", ConsoleColor.Green);
activeHosts.Add(info.ToArray()); }
activeHosts.Add([.. info]);
} }
else if (fail) else if (fail)
{
if (!options.Silent && !suppressPerIpOutput)
{ {
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Failed: {ipAddress}", ConsoleColor.Red); ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Failed: {ipAddress}", ConsoleColor.Red);
} }
}
else else
{
if (!options.Silent && !suppressPerIpOutput)
{ {
ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Inactive: {ipAddress}", ConsoleColor.Red); ConsoleExt.WriteLine($"Progress: {localProcessedIpAddressesCount.ToString().PadLeft(numberOfDigits)}/{ipAddressesCount} [{100d / ipAddressesCount * localProcessedIpAddressesCount,6:##0.00}%] | Inactive: {ipAddress}", ConsoleColor.Red);
} }
}
}); });
if (!activeHosts.IsEmpty) if (previouslyActiveHosts.Count > 0 && !options.Silent)
{ {
Console.WriteLine(Environment.NewLine + "Active hosts:"); PrintDifference(activeHosts, header, options);
}
else if (!options.Silent)
{
PrintActiveHosts(activeHosts, header, options);
}
List<string[]>? activeHostsTable = activeHosts.ToList(); if (!options.Silent && (options.JsonPath is not null || options.CsvPath is not null))
{
Console.WriteLine();
}
activeHostsTable.Insert(0, header.ToArray()); if (options.JsonPath is not null)
{
string jsonPath = GetOutputPath(options.JsonPath, ".json");
activeHostsTable.ToArray().To2D().PrintTable(TableStyle.List); try
{
if (!string.IsNullOrWhiteSpace(Path.GetDirectoryName(jsonPath)))
{
_ = Directory.CreateDirectory(Path.GetDirectoryName(jsonPath)!);
}
ConsoleExt.WriteLine($"{Environment.NewLine}Found {activeHosts.Count} active hosts", ConsoleColor.Green); string json = JsonSerializer.Serialize(JsonResult.Parse([.. activeHosts], previouslyActiveHosts), jsonSerializerOptions);
File.WriteAllText(jsonPath, json);
if (!options.Silent)
{
ConsoleExt.WriteLine($"Saved JSON to '{jsonPath}'", ConsoleColor.Green);
}
}
catch (Exception ex)
{
if (!options.Silent)
{
ConsoleExt.WriteLine($"Failed to save JSON to '{jsonPath}': {ex.Message}", ConsoleColor.Red);
}
exitCode = 3;
}
}
if (options.CsvPath is not null)
{
string csvPath = GetOutputPath(options.CsvPath, ".csv");
List<string[]>? activeHostsTable = [.. activeHosts];
activeHostsTable.Insert(0, [.. header]);
try
{
if (!string.IsNullOrWhiteSpace(Path.GetDirectoryName(csvPath)))
{
_ = Directory.CreateDirectory(Path.GetDirectoryName(csvPath)!);
}
File.WriteAllLines(csvPath, activeHostsTable.Select(row => string.Join(",", row)));
if (!options.Silent)
{
ConsoleExt.WriteLine($"Saved CSV to '{csvPath}'", ConsoleColor.Green);
}
}
catch (Exception ex)
{
if (!options.Silent)
{
ConsoleExt.WriteLine($"Failed to save CSV to '{csvPath}': {ex.Message}", ConsoleColor.Red);
}
exitCode = 3;
}
}
previouslyActiveHosts.Clear();
foreach (string[] activeHost in activeHosts)
{
previouslyActiveHosts.Add(activeHost);
}
return exitCode;
}
private static Task<int> HandleParseError(IEnumerable<Error> errors)
{
if (errors.IsHelp() || errors.IsVersion())
{
return Task.FromResult(0);
} }
else else
{ {
ConsoleExt.WriteLine($"{Environment.NewLine}No active hosts found", ConsoleColor.Red); return Task.FromResult(1);
}
}
private static string GetOutputPath(string path, string extension)
{
string directory = Path.GetDirectoryName(path) ?? string.Empty;
string fileName = Path.GetFileNameWithoutExtension(path);
string fileExtension = Path.GetExtension(path);
fileExtension = string.IsNullOrEmpty(fileExtension) ? extension : fileExtension;
string dateTime = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
string newPath = Path.Combine(directory, $"{fileName}_{dateTime}{fileExtension}");
int counter = 0;
while (File.Exists(newPath))
{
newPath = Path.Combine(directory, $"{fileName}_{dateTime}_{++counter}{fileExtension}");
}
return newPath;
}
private static void PrintActiveHosts(ConcurrentBag<string[]> activeHosts, List<string> header, ScanOptions options)
{
IEnumerable<string[]> filtered = FilterByVendorAndMacPrefix(activeHosts, options);
List<string[]> activeHostsTable = [.. filtered];
activeHostsTable.Insert(0, [.. header]);
Console.WriteLine();
int filteredCount = GetHostCount(activeHostsTable);
if (filteredCount > 0)
{
PrintSection("Active hosts", "active host", activeHostsTable, options, ConsoleColor.Green);
}
else
{
ConsoleExt.WriteLine("No active hosts found", ConsoleColor.Red);
}
}
private static void PrintDifference(ConcurrentBag<string[]> activeHosts, List<string> header, ScanOptions options)
{
List<string[]> newHosts = activeHosts.Where(activeHost => !previouslyActiveHosts.Exists(previousHost => previousHost[1] == activeHost[1])).ToList();
List<string[]> removedHosts = previouslyActiveHosts.Where(previousHost => !activeHosts.Any(activeHost => activeHost[1] == previousHost[1])).ToList();
List<string[]> newHostsTable = [.. FilterByVendorAndMacPrefix(newHosts, options)];
newHostsTable.Insert(0, [.. header]);
List<string[]> removedHostsTable = [.. FilterByVendorAndMacPrefix(removedHosts, options)];
removedHostsTable.Insert(0, [.. header]);
Console.WriteLine();
bool isMonitoring = options is MonitorOptions;
int newCount = GetHostCount(newHostsTable);
if (newCount > 0)
{
PrintSection("New hosts", "active host", newHostsTable, options, ConsoleColor.Green, " (summary mode)");
}
else
{
ConsoleExt.WriteLine("No new hosts found", ConsoleColor.Blue);
}
Console.WriteLine();
int removedCount = GetHostCount(removedHostsTable);
if (removedCount > 0)
{
// Better term for removed host?
PrintSection("Previously active hosts", "previously active host", removedHostsTable, options, ConsoleColor.Red, " now inactive (summary mode)");
}
else
{
ConsoleExt.WriteLine("No previously active hosts are currently inactive", ConsoleColor.Blue);
}
if (isMonitoring && options is MonitorOptions monitorOptions)
{
// Execute user-defined commands for new and removed hosts, if configured.
JsonResult jsonResult = JsonResult.Parse([.. activeHosts], previouslyActiveHosts);
if (newHosts.Count > 0 && !string.IsNullOrWhiteSpace(monitorOptions.OnNewCommand))
{
ExecuteMonitorCommand(monitorOptions.OnNewCommand!, jsonResult.NewHosts);
}
if (removedHosts.Count > 0 && !string.IsNullOrWhiteSpace(monitorOptions.OnRemovedCommand))
{
ExecuteMonitorCommand(monitorOptions.OnRemovedCommand!, jsonResult.RemovedHosts);
}
}
}
private static IEnumerable<string[]> FilterByVendorAndMacPrefix(IEnumerable<string[]> hosts, ScanOptions options)
{
IEnumerable<string[]> filtered = hosts;
if (!string.IsNullOrWhiteSpace(options.VendorFilter))
{
string vendorFilter = options.VendorFilter.Trim();
filtered = filtered.Where(h => h.Length > 2 && h[2].Contains(vendorFilter, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(options.MacPrefixFilter))
{
string macPrefix = options.MacPrefixFilter.Trim().Replace("-", ":").ToUpperInvariant();
filtered = filtered.Where(h => h.Length > 1 && h[1].Replace("-", ":").ToUpperInvariant().StartsWith(macPrefix, StringComparison.Ordinal));
}
return filtered;
}
private static int GetHostCount(List<string[]> tableWithHeader)
{
// subtract one for header row
return Math.Max(0, tableWithHeader.Count - 1);
}
private static void PrintSection(string title, string itemLabel, List<string[]> tableWithHeader, ScanOptions options, ConsoleColor color, string? summarySuffix = null)
{
int count = GetHostCount(tableWithHeader);
if (count <= 0)
{
return;
}
if (IsSummaryMode(options))
{
string suffix = summarySuffix ?? " (summary mode)";
ConsoleExt.WriteLine($"{title} {itemLabel.ToQuantity(count)}{suffix}", color);
}
else
{
Console.WriteLine(title + ":");
tableWithHeader.ToArray().To2D().PrintTable(TableStyle.List);
ConsoleExt.WriteLine($"{Environment.NewLine}Found {itemLabel.ToQuantity(count)}", color);
}
}
private static bool IsSummaryMode(ScanOptions options)
{
return options is MonitorOptions monitorOptions && monitorOptions.Summary;
}
private static List<IPAddress> GetLocalSubnets()
{
List<IPAddress> addresses = [];
foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces())
{
if (networkInterface.OperationalStatus != OperationalStatus.Up)
{
continue;
}
IPInterfaceProperties ipProperties = networkInterface.GetIPProperties();
foreach (UnicastIPAddressInformation unicast in ipProperties.UnicastAddresses)
{
if (unicast.Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
{
continue;
}
int prefixLength = unicast.PrefixLength;
if (prefixLength is <= 0 or > 32)
{
continue;
}
uint mask = prefixLength == 0 ? 0 : uint.MaxValue << (32 - prefixLength);
byte[] ipBytes = unicast.Address.GetAddressBytes();
uint ip = (uint)((ipBytes[0] << 24) | (ipBytes[1] << 16) | (ipBytes[2] << 8) | ipBytes[3]);
uint network = ip & mask;
uint broadcast = network | ~mask;
for (uint addr = network + 1; addr < broadcast; addr++)
{
byte[] addrBytes =
[
(byte)((addr >> 24) & 0xFF),
(byte)((addr >> 16) & 0xFF),
(byte)((addr >> 8) & 0xFF),
(byte)(addr & 0xFF)
];
addresses.Add(new IPAddress(addrBytes));
}
}
}
return addresses;
}
private static void ExecuteMonitorCommand(string command, IEnumerable<JsonResult.HostInformation> hosts)
{
try
{
ProcessStartInfo psi = new()
{
FileName = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh",
Arguments = OperatingSystem.IsWindows() ? $"/C {command}" : $"-c \"{command}\"",
RedirectStandardInput = true,
RedirectStandardOutput = false,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true
};
using Process process = Process.Start(psi)!;
string json = JsonSerializer.Serialize(hosts, jsonSerializerOptions);
process.StandardInput.WriteLine(json);
process.StandardInput.Close();
}
catch (Exception ex)
{
ConsoleExt.WriteLine($"Failed to execute monitor command '{command}': {ex.Message}", ConsoleColor.Red);
} }
} }
} }
@@ -2,7 +2,7 @@
"profiles": { "profiles": {
"ARP-Scanner": { "ARP-Scanner": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "192.168.1.0 - 192.168.1.255" "commandLineArgs": "scan 192.168.1.239"
} }
} }
} }
+34
View File
@@ -0,0 +1,34 @@
using CommandLine;
namespace ARP_Scanner;
[Verb("scan", true, HelpText = "Scan the specified IP range.")]
internal class ScanOptions
{
[Value(0, Required = false, MetaName = "IP range", HelpText = "The IP range to scan.")]
public string? IpRange { get; set; }
[Option('s', "silent", Required = false, HelpText = "Don't print anything to the console.")]
public bool Silent { get; set; }
[Option('r', "retry", Required = false, Default = 0, HelpText = "The number of retries for each ARP request.")]
public int Retry { get; set; }
[Option('c', "concurrency", Required = false, Default = -1, HelpText = "The number of concurrent ARP requests.")]
public int Concurrency { get; set; }
[Option("json", Required = false, HelpText = "The path to the JSON file to save the results.")]
public string? JsonPath { get; set; }
[Option("csv", Required = false, HelpText = "The path to the CSV file to save the results.")]
public string? CsvPath { get; set; }
[Option("auto", Required = false, HelpText = "Automatically scan all local IPv4 subnets when no IP range is specified.")]
public bool AutoDiscover { get; set; }
[Option("vendor", Required = false, HelpText = "Filter results by vendor name (case-insensitive substring).")]
public string? VendorFilter { get; set; }
[Option("mac-prefix", Required = false, HelpText = "Filter results by MAC prefix (e.g., 00:11:22).")]
public string? MacPrefixFilter { get; set; }
}
File diff suppressed because it is too large Load Diff