ipmg 2.0.0__tar.gz → 2.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ipmg-2.0.0/src/ipmg.egg-info → ipmg-2.1.1}/PKG-INFO +82 -15
- {ipmg-2.0.0 → ipmg-2.1.1}/README.md +78 -12
- {ipmg-2.0.0 → ipmg-2.1.1}/pyproject.toml +13 -3
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/__init__.py +1 -1
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/cli/parser.py +26 -2
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/core/engine.py +53 -14
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/core/portscan.py +19 -10
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/infrastructure/file_io.py +45 -15
- ipmg-2.1.1/src/ipmg/infrastructure/incremental.py +276 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/services/scan_service.py +89 -6
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/utils/helpers.py +5 -1
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/app.py +75 -9
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/manager.py +24 -2
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/server.py +37 -5
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/static/index.html +2 -2
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/static/js/api.js +63 -2
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/static/js/app.js +2 -2
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/static/js/views.js +25 -8
- {ipmg-2.0.0 → ipmg-2.1.1/src/ipmg.egg-info}/PKG-INFO +82 -15
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg.egg-info/SOURCES.txt +3 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg.egg-info/requires.txt +2 -1
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_engine.py +54 -3
- ipmg-2.1.1/tests/test_incremental.py +227 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_scan_service.py +11 -11
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_utils.py +12 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_web_api.py +95 -3
- ipmg-2.1.1/tests/test_web_manager.py +36 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_web_server.py +51 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/LICENSE +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/setup.cfg +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/__main__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/cli/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/cli/commands.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/core/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/core/diff.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/core/discovery.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/core/ping.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/core/security.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/exceptions.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/infrastructure/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/infrastructure/database.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/reporting/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/reporting/diff_report.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/reporting/frames.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/reporting/live.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/reporting/summary.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/reporting/ui.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/services/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/services/history_service.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/utils/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/db.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/static/css/app.css +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/static/js/charts.js +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg/web/static/js/demo.js +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg.egg-info/dependency_links.txt +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg.egg-info/entry_points.txt +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/src/ipmg.egg-info/top_level.txt +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_commands.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_database_history.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_diff.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_diff_report.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_discover.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_file_io.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_history_service.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_live.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_parser.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_ping.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_ping_command.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_portscan.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_ui.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.1}/tests/test_web_db.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipmg
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: IP Management & Ping Monitoring CLI Tool
|
|
5
5
|
Author: Sameer Alam
|
|
6
6
|
Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
|
|
@@ -10,7 +10,7 @@ Project-URL: Documentation, https://github.com/sameeralam3127/ipmg#readme
|
|
|
10
10
|
Project-URL: Repository, https://github.com/sameeralam3127/ipmg
|
|
11
11
|
Project-URL: Issues, https://github.com/sameeralam3127/ipmg/issues
|
|
12
12
|
Project-URL: Changelog, https://github.com/sameeralam3127/ipmg/releases
|
|
13
|
-
Keywords: ip,network,ping,monitoring,cli,port-scan
|
|
13
|
+
Keywords: ip,network,ping,monitoring,cli,port-scan,network-scanner,ping-sweep,subnet-scanner,homelab,sysadmin
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -29,7 +29,7 @@ Classifier: Environment :: Console
|
|
|
29
29
|
Requires-Python: >=3.9
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
License-File: LICENSE
|
|
32
|
-
Requires-Dist: pandas>=2.
|
|
32
|
+
Requires-Dist: pandas>=2.2.2
|
|
33
33
|
Requires-Dist: openpyxl>=3.1
|
|
34
34
|
Requires-Dist: rich>=13.0
|
|
35
35
|
Requires-Dist: fastapi>=0.110
|
|
@@ -38,6 +38,7 @@ Requires-Dist: websockets>=12
|
|
|
38
38
|
Requires-Dist: python-multipart>=0.0.9
|
|
39
39
|
Provides-Extra: dev
|
|
40
40
|
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
41
42
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
42
43
|
Requires-Dist: pre-commit>=3.5.0; extra == "dev"
|
|
43
44
|
Requires-Dist: python-semantic-release>=10.5.3; extra == "dev"
|
|
@@ -58,8 +59,19 @@ can send to someone: Excel, CSV, JSON, or Markdown. It works from the command
|
|
|
58
59
|
line or from IPMG Web, a local browser UI, and it remembers every scan so it can tell
|
|
59
60
|
you what moved.
|
|
60
61
|
|
|
62
|
+
<p align="center">
|
|
63
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-demo.gif" alt="ipmg scanning 13 hosts in parallel: live results with reverse DNS names and latency, then a summary of 10 active and 3 timed out" width="820">
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
**Why not `nmap -sn`, `fping`, or Angry IP Scanner?** They tell you what is up
|
|
67
|
+
right now. IPMG also remembers every scan and tells you what changed since the
|
|
68
|
+
last one: the host that dropped off, the device that appeared, the latency that
|
|
69
|
+
doubled. It writes the report you would otherwise build by hand.
|
|
70
|
+
[How it compares](#how-it-compares)
|
|
71
|
+
|
|
61
72
|
**Website:** [sameeralam3127.github.io/ipmg](https://sameeralam3127.github.io/ipmg/) ·
|
|
62
|
-
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/)
|
|
73
|
+
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/) ·
|
|
74
|
+
**Command builder:** [pick what you want, copy the command](https://sameeralam3127.github.io/ipmg/#builder)
|
|
63
75
|
|
|
64
76
|
```bash
|
|
65
77
|
pip install ipmg
|
|
@@ -71,6 +83,7 @@ ipmg --discover # scan the network you are on, right now
|
|
|
71
83
|
|
|
72
84
|
**Contents**
|
|
73
85
|
|
|
86
|
+
[How it compares](#how-it-compares) ·
|
|
74
87
|
[Install](#install) ·
|
|
75
88
|
[Your first scan](#your-first-scan) ·
|
|
76
89
|
[Common tasks](#common-tasks) ·
|
|
@@ -86,6 +99,26 @@ ipmg --discover # scan the network you are on, right now
|
|
|
86
99
|
|
|
87
100
|
---
|
|
88
101
|
|
|
102
|
+
## How it compares
|
|
103
|
+
|
|
104
|
+
| | IPMG | `nmap -sn` | `fping` | Angry IP Scanner |
|
|
105
|
+
| --- | --- | --- | --- | --- |
|
|
106
|
+
| Parallel ping sweep | Yes | Yes | Yes | Yes |
|
|
107
|
+
| Scan history and "what changed" | Built in | Save XML, compare with `ndiff` | No | No |
|
|
108
|
+
| Reports | Excel, CSV, JSON, Markdown | XML, grepable text | Plain text | CSV, TXT, XML |
|
|
109
|
+
| Browser UI | IPMG Web, local | No (Zenmap is a desktop app) | No | Desktop app (Java) |
|
|
110
|
+
| Port checks | Common TCP ports | Full port and OS scanner | No | Via fetchers |
|
|
111
|
+
|
|
112
|
+
Reach for nmap when you need a real port or OS scanner. Reach for IPMG when you
|
|
113
|
+
look after a network and need to know what moved since yesterday, with a report
|
|
114
|
+
you can hand to someone.
|
|
115
|
+
|
|
116
|
+
<p align="center">
|
|
117
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-web.png" alt="IPMG Web dashboard: scan totals, a status donut of 16 active, 1 timeout and 1 inactive host, a latency trend chart, and a list of recent scans" width="820">
|
|
118
|
+
</p>
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
89
122
|
## Install
|
|
90
123
|
|
|
91
124
|
**Linux and macOS — one command, works on every distribution:**
|
|
@@ -280,6 +313,19 @@ exist yet. A file you name with `--input` must already exist.
|
|
|
280
313
|
|
|
281
314
|
## Common tasks
|
|
282
315
|
|
|
316
|
+
Not sure which flags you need? The
|
|
317
|
+
[command builder](https://sameeralam3127.github.io/ipmg/#builder) on the
|
|
318
|
+
website puts the command together as you pick what you want to know, and
|
|
319
|
+
explains every flag it adds.
|
|
320
|
+
|
|
321
|
+
<p align="center">
|
|
322
|
+
<a href="https://sameeralam3127.github.io/ipmg/#builder">
|
|
323
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-builder.png" alt="IPMG command builder: choose Scan, Compare, History, or Web, enter a target such as 192.168.1.0/24, switch on options like live results or hostnames, and copy the generated ipmg command with each flag explained" width="820">
|
|
324
|
+
</a>
|
|
325
|
+
</p>
|
|
326
|
+
|
|
327
|
+
Or pick a ready-made command:
|
|
328
|
+
|
|
283
329
|
| I want to… | Command |
|
|
284
330
|
| --- | --- |
|
|
285
331
|
| Scan the network I am on | `ipmg --discover` |
|
|
@@ -390,6 +436,12 @@ ipmg web # starts http://127.0.0.1:8080 and opens your browser
|
|
|
390
436
|
|
|
391
437
|
`ipmg --web` does the same thing, so whichever one you reach for first works.
|
|
392
438
|
|
|
439
|
+
Each start creates a new access token. The browser opens with it already in
|
|
440
|
+
the link, and IPMG Web prints that link (`http://127.0.0.1:8080/#token=…`)
|
|
441
|
+
in the terminal. If you open IPMG Web in another browser, or after a
|
|
442
|
+
restart, use the link from the terminal. To keep the same token across
|
|
443
|
+
restarts, for example behind a reverse proxy, set `IPMG_WEB_TOKEN`.
|
|
444
|
+
|
|
393
445
|
It runs fully offline — every stylesheet and script is bundled with the
|
|
394
446
|
package, nothing is loaded from a CDN. It gives you:
|
|
395
447
|
|
|
@@ -419,12 +471,13 @@ it from your workstation with an SSH tunnel:
|
|
|
419
471
|
|
|
420
472
|
```bash
|
|
421
473
|
ssh -L 8080:127.0.0.1:8080 user@server
|
|
422
|
-
# then open http://127.0.0.1:8080 locally
|
|
474
|
+
# then open the link the server printed (http://127.0.0.1:8080/#token=…) locally
|
|
423
475
|
```
|
|
424
476
|
|
|
425
|
-
Alternatively, bind to all interfaces with `--host 0.0.0.0
|
|
426
|
-
|
|
427
|
-
|
|
477
|
+
Alternatively, bind to all interfaces with `--host 0.0.0.0`. Every request
|
|
478
|
+
still needs the access token. The traffic, token included, is plain HTTP,
|
|
479
|
+
so on anything but a trusted network put a reverse proxy with TLS in front
|
|
480
|
+
of it (see [Security](#security)).
|
|
428
481
|
|
|
429
482
|
---
|
|
430
483
|
|
|
@@ -478,7 +531,16 @@ Status is one of `Active`, `Inactive`, `Timeout`, `Unreachable`, `Invalid IP`,
|
|
|
478
531
|
or `Error`. `Hostname` is filled in when you pass `--resolve`.
|
|
479
532
|
|
|
480
533
|
The `md` format produces a shareable Markdown report with a status summary
|
|
481
|
-
table — handy for tickets, handoffs, and incident timelines.
|
|
534
|
+
table — handy for tickets, handoffs, and incident timelines. `jsonl` writes one
|
|
535
|
+
JSON object per line, which streams into `jq` and log pipelines.
|
|
536
|
+
|
|
537
|
+
**Reports survive an interrupted scan.** A scan writes its report as it goes,
|
|
538
|
+
so pressing Ctrl+C halfway through a /16 leaves a valid report of everything
|
|
539
|
+
scanned so far instead of nothing at all. `csv` and `jsonl` are appended per
|
|
540
|
+
host; `xlsx`, `json`, and `md` are re-saved every `--autosave` seconds (30 by
|
|
541
|
+
default). A finished scan overwrites those files with the complete report, so
|
|
542
|
+
the file names and contents are the same as they always were. Use
|
|
543
|
+
`--no-incremental` to go back to writing only at the end.
|
|
482
544
|
|
|
483
545
|
**Open ports.** `Open Ports` is only populated when `--scan-ports` is set: for
|
|
484
546
|
each host that answers, IPMG probes a list of common TCP ports (SSH, HTTP,
|
|
@@ -507,7 +569,9 @@ them — so piping IPMG into a file or a log gives you clean text.
|
|
|
507
569
|
| `--input` | `ip_list.xlsx` | What to scan: a file (`.xlsx`, `.xls`, `.csv`, `.txt`, `.list`), a single IP, a CIDR block, or a range (`10.0.0.1-10.0.0.50`) |
|
|
508
570
|
| `--discover` | off | Auto-detect and scan the local subnet instead |
|
|
509
571
|
| `--output` | `results` | Report file name prefix |
|
|
510
|
-
| `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `md` |
|
|
572
|
+
| `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` |
|
|
573
|
+
| `--no-incremental` | off | Only write the report once the scan has finished |
|
|
574
|
+
| `--autosave` | `30` | How often a running scan re-saves `xlsx`, `json`, and `md` |
|
|
511
575
|
|
|
512
576
|
**Speed and accuracy**
|
|
513
577
|
|
|
@@ -555,15 +619,18 @@ itself is hardened accordingly:
|
|
|
555
619
|
validated as an IP address first
|
|
556
620
|
- IPMG Web binds to `127.0.0.1` by default and serves everything
|
|
557
621
|
locally — no CDN assets, no outbound requests
|
|
558
|
-
-
|
|
559
|
-
|
|
622
|
+
- Every API request and WebSocket needs the access token created when
|
|
623
|
+
IPMG Web starts, so other users on the machine and web pages you happen
|
|
624
|
+
to visit cannot start scans or read your results
|
|
625
|
+
- WebSocket connections are also origin-checked, and a client that stops
|
|
626
|
+
reading live updates is disconnected rather than buffered without limit
|
|
560
627
|
- Uploads are capped at 5 MB and one scan expands to at most 65,536 hosts,
|
|
561
628
|
so a bad input file cannot exhaust memory
|
|
562
629
|
- All database access uses parameterized SQL
|
|
563
630
|
|
|
564
|
-
If you bind to a non-local address with `--host`,
|
|
565
|
-
|
|
566
|
-
|
|
631
|
+
If you bind to a non-local address with `--host`, the token still guards the
|
|
632
|
+
API. It travels over plain HTTP, though, so use an SSH tunnel or a reverse
|
|
633
|
+
proxy with TLS on any network you don't trust.
|
|
567
634
|
|
|
568
635
|
Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
|
|
569
636
|
|
|
@@ -12,8 +12,19 @@ can send to someone: Excel, CSV, JSON, or Markdown. It works from the command
|
|
|
12
12
|
line or from IPMG Web, a local browser UI, and it remembers every scan so it can tell
|
|
13
13
|
you what moved.
|
|
14
14
|
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-demo.gif" alt="ipmg scanning 13 hosts in parallel: live results with reverse DNS names and latency, then a summary of 10 active and 3 timed out" width="820">
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
**Why not `nmap -sn`, `fping`, or Angry IP Scanner?** They tell you what is up
|
|
20
|
+
right now. IPMG also remembers every scan and tells you what changed since the
|
|
21
|
+
last one: the host that dropped off, the device that appeared, the latency that
|
|
22
|
+
doubled. It writes the report you would otherwise build by hand.
|
|
23
|
+
[How it compares](#how-it-compares)
|
|
24
|
+
|
|
15
25
|
**Website:** [sameeralam3127.github.io/ipmg](https://sameeralam3127.github.io/ipmg/) ·
|
|
16
|
-
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/)
|
|
26
|
+
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/) ·
|
|
27
|
+
**Command builder:** [pick what you want, copy the command](https://sameeralam3127.github.io/ipmg/#builder)
|
|
17
28
|
|
|
18
29
|
```bash
|
|
19
30
|
pip install ipmg
|
|
@@ -25,6 +36,7 @@ ipmg --discover # scan the network you are on, right now
|
|
|
25
36
|
|
|
26
37
|
**Contents**
|
|
27
38
|
|
|
39
|
+
[How it compares](#how-it-compares) ·
|
|
28
40
|
[Install](#install) ·
|
|
29
41
|
[Your first scan](#your-first-scan) ·
|
|
30
42
|
[Common tasks](#common-tasks) ·
|
|
@@ -40,6 +52,26 @@ ipmg --discover # scan the network you are on, right now
|
|
|
40
52
|
|
|
41
53
|
---
|
|
42
54
|
|
|
55
|
+
## How it compares
|
|
56
|
+
|
|
57
|
+
| | IPMG | `nmap -sn` | `fping` | Angry IP Scanner |
|
|
58
|
+
| --- | --- | --- | --- | --- |
|
|
59
|
+
| Parallel ping sweep | Yes | Yes | Yes | Yes |
|
|
60
|
+
| Scan history and "what changed" | Built in | Save XML, compare with `ndiff` | No | No |
|
|
61
|
+
| Reports | Excel, CSV, JSON, Markdown | XML, grepable text | Plain text | CSV, TXT, XML |
|
|
62
|
+
| Browser UI | IPMG Web, local | No (Zenmap is a desktop app) | No | Desktop app (Java) |
|
|
63
|
+
| Port checks | Common TCP ports | Full port and OS scanner | No | Via fetchers |
|
|
64
|
+
|
|
65
|
+
Reach for nmap when you need a real port or OS scanner. Reach for IPMG when you
|
|
66
|
+
look after a network and need to know what moved since yesterday, with a report
|
|
67
|
+
you can hand to someone.
|
|
68
|
+
|
|
69
|
+
<p align="center">
|
|
70
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-web.png" alt="IPMG Web dashboard: scan totals, a status donut of 16 active, 1 timeout and 1 inactive host, a latency trend chart, and a list of recent scans" width="820">
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
43
75
|
## Install
|
|
44
76
|
|
|
45
77
|
**Linux and macOS — one command, works on every distribution:**
|
|
@@ -234,6 +266,19 @@ exist yet. A file you name with `--input` must already exist.
|
|
|
234
266
|
|
|
235
267
|
## Common tasks
|
|
236
268
|
|
|
269
|
+
Not sure which flags you need? The
|
|
270
|
+
[command builder](https://sameeralam3127.github.io/ipmg/#builder) on the
|
|
271
|
+
website puts the command together as you pick what you want to know, and
|
|
272
|
+
explains every flag it adds.
|
|
273
|
+
|
|
274
|
+
<p align="center">
|
|
275
|
+
<a href="https://sameeralam3127.github.io/ipmg/#builder">
|
|
276
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-builder.png" alt="IPMG command builder: choose Scan, Compare, History, or Web, enter a target such as 192.168.1.0/24, switch on options like live results or hostnames, and copy the generated ipmg command with each flag explained" width="820">
|
|
277
|
+
</a>
|
|
278
|
+
</p>
|
|
279
|
+
|
|
280
|
+
Or pick a ready-made command:
|
|
281
|
+
|
|
237
282
|
| I want to… | Command |
|
|
238
283
|
| --- | --- |
|
|
239
284
|
| Scan the network I am on | `ipmg --discover` |
|
|
@@ -344,6 +389,12 @@ ipmg web # starts http://127.0.0.1:8080 and opens your browser
|
|
|
344
389
|
|
|
345
390
|
`ipmg --web` does the same thing, so whichever one you reach for first works.
|
|
346
391
|
|
|
392
|
+
Each start creates a new access token. The browser opens with it already in
|
|
393
|
+
the link, and IPMG Web prints that link (`http://127.0.0.1:8080/#token=…`)
|
|
394
|
+
in the terminal. If you open IPMG Web in another browser, or after a
|
|
395
|
+
restart, use the link from the terminal. To keep the same token across
|
|
396
|
+
restarts, for example behind a reverse proxy, set `IPMG_WEB_TOKEN`.
|
|
397
|
+
|
|
347
398
|
It runs fully offline — every stylesheet and script is bundled with the
|
|
348
399
|
package, nothing is loaded from a CDN. It gives you:
|
|
349
400
|
|
|
@@ -373,12 +424,13 @@ it from your workstation with an SSH tunnel:
|
|
|
373
424
|
|
|
374
425
|
```bash
|
|
375
426
|
ssh -L 8080:127.0.0.1:8080 user@server
|
|
376
|
-
# then open http://127.0.0.1:8080 locally
|
|
427
|
+
# then open the link the server printed (http://127.0.0.1:8080/#token=…) locally
|
|
377
428
|
```
|
|
378
429
|
|
|
379
|
-
Alternatively, bind to all interfaces with `--host 0.0.0.0
|
|
380
|
-
|
|
381
|
-
|
|
430
|
+
Alternatively, bind to all interfaces with `--host 0.0.0.0`. Every request
|
|
431
|
+
still needs the access token. The traffic, token included, is plain HTTP,
|
|
432
|
+
so on anything but a trusted network put a reverse proxy with TLS in front
|
|
433
|
+
of it (see [Security](#security)).
|
|
382
434
|
|
|
383
435
|
---
|
|
384
436
|
|
|
@@ -432,7 +484,16 @@ Status is one of `Active`, `Inactive`, `Timeout`, `Unreachable`, `Invalid IP`,
|
|
|
432
484
|
or `Error`. `Hostname` is filled in when you pass `--resolve`.
|
|
433
485
|
|
|
434
486
|
The `md` format produces a shareable Markdown report with a status summary
|
|
435
|
-
table — handy for tickets, handoffs, and incident timelines.
|
|
487
|
+
table — handy for tickets, handoffs, and incident timelines. `jsonl` writes one
|
|
488
|
+
JSON object per line, which streams into `jq` and log pipelines.
|
|
489
|
+
|
|
490
|
+
**Reports survive an interrupted scan.** A scan writes its report as it goes,
|
|
491
|
+
so pressing Ctrl+C halfway through a /16 leaves a valid report of everything
|
|
492
|
+
scanned so far instead of nothing at all. `csv` and `jsonl` are appended per
|
|
493
|
+
host; `xlsx`, `json`, and `md` are re-saved every `--autosave` seconds (30 by
|
|
494
|
+
default). A finished scan overwrites those files with the complete report, so
|
|
495
|
+
the file names and contents are the same as they always were. Use
|
|
496
|
+
`--no-incremental` to go back to writing only at the end.
|
|
436
497
|
|
|
437
498
|
**Open ports.** `Open Ports` is only populated when `--scan-ports` is set: for
|
|
438
499
|
each host that answers, IPMG probes a list of common TCP ports (SSH, HTTP,
|
|
@@ -461,7 +522,9 @@ them — so piping IPMG into a file or a log gives you clean text.
|
|
|
461
522
|
| `--input` | `ip_list.xlsx` | What to scan: a file (`.xlsx`, `.xls`, `.csv`, `.txt`, `.list`), a single IP, a CIDR block, or a range (`10.0.0.1-10.0.0.50`) |
|
|
462
523
|
| `--discover` | off | Auto-detect and scan the local subnet instead |
|
|
463
524
|
| `--output` | `results` | Report file name prefix |
|
|
464
|
-
| `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `md` |
|
|
525
|
+
| `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` |
|
|
526
|
+
| `--no-incremental` | off | Only write the report once the scan has finished |
|
|
527
|
+
| `--autosave` | `30` | How often a running scan re-saves `xlsx`, `json`, and `md` |
|
|
465
528
|
|
|
466
529
|
**Speed and accuracy**
|
|
467
530
|
|
|
@@ -509,15 +572,18 @@ itself is hardened accordingly:
|
|
|
509
572
|
validated as an IP address first
|
|
510
573
|
- IPMG Web binds to `127.0.0.1` by default and serves everything
|
|
511
574
|
locally — no CDN assets, no outbound requests
|
|
512
|
-
-
|
|
513
|
-
|
|
575
|
+
- Every API request and WebSocket needs the access token created when
|
|
576
|
+
IPMG Web starts, so other users on the machine and web pages you happen
|
|
577
|
+
to visit cannot start scans or read your results
|
|
578
|
+
- WebSocket connections are also origin-checked, and a client that stops
|
|
579
|
+
reading live updates is disconnected rather than buffered without limit
|
|
514
580
|
- Uploads are capped at 5 MB and one scan expands to at most 65,536 hosts,
|
|
515
581
|
so a bad input file cannot exhaust memory
|
|
516
582
|
- All database access uses parameterized SQL
|
|
517
583
|
|
|
518
|
-
If you bind to a non-local address with `--host`,
|
|
519
|
-
|
|
520
|
-
|
|
584
|
+
If you bind to a non-local address with `--host`, the token still guards the
|
|
585
|
+
API. It travels over plain HTTP, though, so use an SSH tunnel or a reverse
|
|
586
|
+
proxy with TLS on any network you don't trust.
|
|
521
587
|
|
|
522
588
|
Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
|
|
523
589
|
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "ipmg"
|
|
16
|
-
version = "2.
|
|
16
|
+
version = "2.1.1" # Managed automatically by semantic-release
|
|
17
17
|
description = "IP Management & Ping Monitoring CLI Tool"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
requires-python = ">=3.9"
|
|
@@ -28,7 +28,7 @@ maintainers = [
|
|
|
28
28
|
{ name = "Sameer Alam", email = "sameeralam3127@gmail.com" }
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
-
keywords = ["ip", "network", "ping", "monitoring", "cli", "port-scan"]
|
|
31
|
+
keywords = ["ip", "network", "ping", "monitoring", "cli", "port-scan", "network-scanner", "ping-sweep", "subnet-scanner", "homelab", "sysadmin"]
|
|
32
32
|
|
|
33
33
|
classifiers = [
|
|
34
34
|
"Programming Language :: Python :: 3",
|
|
@@ -49,7 +49,7 @@ classifiers = [
|
|
|
49
49
|
]
|
|
50
50
|
|
|
51
51
|
dependencies = [
|
|
52
|
-
"pandas>=2.
|
|
52
|
+
"pandas>=2.2.2",
|
|
53
53
|
"openpyxl>=3.1",
|
|
54
54
|
"rich>=13.0",
|
|
55
55
|
"fastapi>=0.110",
|
|
@@ -68,6 +68,7 @@ Changelog = "https://github.com/sameeralam3127/ipmg/releases"
|
|
|
68
68
|
[project.optional-dependencies]
|
|
69
69
|
dev = [
|
|
70
70
|
"pytest>=7.4",
|
|
71
|
+
"pytest-cov>=5.0",
|
|
71
72
|
"ruff>=0.4",
|
|
72
73
|
"pre-commit>=3.5.0",
|
|
73
74
|
"python-semantic-release>=10.5.3",
|
|
@@ -85,6 +86,7 @@ ipmg = "ipmg.cli.commands:run"
|
|
|
85
86
|
[dependency-groups]
|
|
86
87
|
dev = [
|
|
87
88
|
"pytest>=7.4",
|
|
89
|
+
"pytest-cov>=5.0",
|
|
88
90
|
"ruff>=0.4",
|
|
89
91
|
"pre-commit>=3.5.0",
|
|
90
92
|
"python-semantic-release>=10.5.3",
|
|
@@ -130,6 +132,14 @@ where = ["src"]
|
|
|
130
132
|
testpaths = ["tests"]
|
|
131
133
|
pythonpath = ["src"]
|
|
132
134
|
|
|
135
|
+
[tool.coverage.run]
|
|
136
|
+
source = ["ipmg"]
|
|
137
|
+
branch = true
|
|
138
|
+
|
|
139
|
+
[tool.coverage.report]
|
|
140
|
+
# Current coverage is ~95%; this floor catches a large untested addition.
|
|
141
|
+
fail_under = 90
|
|
142
|
+
|
|
133
143
|
|
|
134
144
|
# ==========================================================
|
|
135
145
|
# Ruff
|
|
@@ -7,6 +7,12 @@ import argparse
|
|
|
7
7
|
from ipmg import __version__
|
|
8
8
|
from ipmg.core.portscan import DEFAULT_PORTS, parse_port_list
|
|
9
9
|
from ipmg.infrastructure.file_io import DEFAULT_INPUT_FILE
|
|
10
|
+
from ipmg.infrastructure.incremental import (
|
|
11
|
+
DEFAULT_AUTOSAVE_S,
|
|
12
|
+
MAX_AUTOSAVE_S,
|
|
13
|
+
MIN_AUTOSAVE_S,
|
|
14
|
+
REPORT_FORMATS,
|
|
15
|
+
)
|
|
10
16
|
from ipmg.reporting.diff_report import DIFF_FORMATS
|
|
11
17
|
from ipmg.reporting.live import DEFAULT_REFRESH_S, MAX_REFRESH_S, MIN_REFRESH_S
|
|
12
18
|
|
|
@@ -135,9 +141,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
135
141
|
"--formats",
|
|
136
142
|
nargs="+",
|
|
137
143
|
default=["xlsx"],
|
|
138
|
-
choices=
|
|
144
|
+
choices=list(REPORT_FORMATS),
|
|
139
145
|
metavar="FORMAT",
|
|
140
|
-
help="One or more report formats:
|
|
146
|
+
help=f"One or more report formats: {', '.join(REPORT_FORMATS)} (default: xlsx).",
|
|
141
147
|
)
|
|
142
148
|
parser.add_argument(
|
|
143
149
|
"--discover",
|
|
@@ -196,6 +202,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
196
202
|
help="Connect timeout per port when --scan-ports is set (default: 1).",
|
|
197
203
|
)
|
|
198
204
|
|
|
205
|
+
reports = parser.add_argument_group("report writing")
|
|
206
|
+
reports.add_argument(
|
|
207
|
+
"--no-incremental",
|
|
208
|
+
action="store_true",
|
|
209
|
+
help="Only write the report once the scan has finished.",
|
|
210
|
+
)
|
|
211
|
+
reports.add_argument(
|
|
212
|
+
"--autosave",
|
|
213
|
+
type=float,
|
|
214
|
+
default=DEFAULT_AUTOSAVE_S,
|
|
215
|
+
metavar="SECONDS",
|
|
216
|
+
help=(
|
|
217
|
+
"How often a running scan re-saves the xlsx, json, and md reports "
|
|
218
|
+
f"(default: {DEFAULT_AUTOSAVE_S:g}, range: {MIN_AUTOSAVE_S:g}-{MAX_AUTOSAVE_S:g}). "
|
|
219
|
+
"csv and jsonl are written per host regardless."
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
199
223
|
live = parser.add_argument_group("live output")
|
|
200
224
|
live.add_argument(
|
|
201
225
|
"--stream",
|
|
@@ -11,6 +11,11 @@ from ipmg.core.portscan import DEFAULT_PORTS, scan_ports
|
|
|
11
11
|
from ipmg.exceptions import PingError
|
|
12
12
|
from ipmg.utils.helpers import HostnameCache, clamp_int
|
|
13
13
|
|
|
14
|
+
#: Upper bound on TCP connect probes in flight across a whole scan. Every host
|
|
15
|
+
#: worker shares this one pool, so --scan-ports cannot create
|
|
16
|
+
#: threads x ports probe threads.
|
|
17
|
+
MAX_PORT_PROBE_WORKERS = 128
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
@dataclass(frozen=True)
|
|
16
21
|
class ScanConfig:
|
|
@@ -65,37 +70,71 @@ def execute_scan(
|
|
|
65
70
|
cache = HostnameCache(config.dns_cache_ttl) if config.resolve else None
|
|
66
71
|
results: List[HostResult] = []
|
|
67
72
|
|
|
73
|
+
port_executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
|
|
74
|
+
if config.scan_ports and config.ports:
|
|
75
|
+
port_executor = concurrent.futures.ThreadPoolExecutor(
|
|
76
|
+
max_workers=min(MAX_PORT_PROBE_WORKERS, config.threads * len(config.ports)),
|
|
77
|
+
thread_name_prefix="ipmg-port",
|
|
78
|
+
)
|
|
79
|
+
|
|
68
80
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=config.threads)
|
|
69
81
|
try:
|
|
70
|
-
futures =
|
|
82
|
+
futures = [executor.submit(_probe_host, ip, config, cache, port_executor) for ip in ips]
|
|
71
83
|
|
|
72
84
|
for future in concurrent.futures.as_completed(futures):
|
|
73
85
|
if should_stop is not None and should_stop():
|
|
74
86
|
executor.shutdown(wait=False, cancel_futures=True)
|
|
75
87
|
break
|
|
76
88
|
|
|
77
|
-
ip = futures[future]
|
|
78
89
|
try:
|
|
79
|
-
|
|
90
|
+
result = future.result()
|
|
80
91
|
except PingError:
|
|
81
92
|
executor.shutdown(wait=False, cancel_futures=True)
|
|
82
93
|
raise
|
|
83
|
-
except Exception:
|
|
84
|
-
status, latency = "Error", None
|
|
85
|
-
|
|
86
|
-
hostname = cache.resolve(ip) if cache else ""
|
|
87
|
-
open_ports: Tuple[int, ...] = ()
|
|
88
|
-
if config.scan_ports and status == "Active":
|
|
89
|
-
open_ports = tuple(scan_ports(ip, config.ports, config.port_timeout))
|
|
90
|
-
|
|
91
|
-
result = HostResult(
|
|
92
|
-
ip=ip, status=status, latency=latency, hostname=hostname, open_ports=open_ports
|
|
93
|
-
)
|
|
94
94
|
results.append(result)
|
|
95
95
|
|
|
96
96
|
if on_result is not None:
|
|
97
97
|
on_result(result, len(results), len(ips))
|
|
98
|
+
except BaseException:
|
|
99
|
+
# Ctrl+C must not be followed by minutes of queued pings: drop the
|
|
100
|
+
# hosts that have not started, and let the finally below wait only
|
|
101
|
+
# for the handful already in flight.
|
|
102
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
103
|
+
raise
|
|
98
104
|
finally:
|
|
99
105
|
executor.shutdown(wait=True)
|
|
106
|
+
if port_executor is not None:
|
|
107
|
+
port_executor.shutdown(wait=True)
|
|
100
108
|
|
|
101
109
|
return results
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _probe_host(
|
|
113
|
+
ip: str,
|
|
114
|
+
config: ScanConfig,
|
|
115
|
+
cache: Optional[HostnameCache],
|
|
116
|
+
port_executor: Optional[concurrent.futures.Executor],
|
|
117
|
+
) -> HostResult:
|
|
118
|
+
"""Ping one host, then resolve its name and probe its ports.
|
|
119
|
+
|
|
120
|
+
Runs on a pool worker, so reverse DNS is spread across ``config.threads``
|
|
121
|
+
workers instead of running one host at a time on the thread that collects
|
|
122
|
+
results. Port probes go to the scan's shared, bounded ``port_executor``.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
status, latency = ping_ip(ip, config.timeout, config.count)
|
|
126
|
+
except PingError:
|
|
127
|
+
raise
|
|
128
|
+
except Exception:
|
|
129
|
+
status, latency = "Error", None
|
|
130
|
+
|
|
131
|
+
hostname = cache.resolve(ip) if cache else ""
|
|
132
|
+
open_ports: Tuple[int, ...] = ()
|
|
133
|
+
if config.scan_ports and status == "Active":
|
|
134
|
+
open_ports = tuple(
|
|
135
|
+
scan_ports(ip, config.ports, config.port_timeout, executor=port_executor)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return HostResult(
|
|
139
|
+
ip=ip, status=status, latency=latency, hostname=hostname, open_ports=open_ports
|
|
140
|
+
)
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import concurrent.futures
|
|
6
6
|
import socket
|
|
7
|
-
from typing import Iterable, List, Tuple
|
|
7
|
+
from typing import Iterable, List, Optional, Tuple
|
|
8
8
|
|
|
9
9
|
#: Common services worth a quick TCP connect once a host is known to be up.
|
|
10
10
|
DEFAULT_PORTS: Tuple[int, ...] = (21, 22, 25, 53, 80, 443, 445, 1433, 3306, 3389, 5432)
|
|
@@ -65,18 +65,27 @@ def parse_port_list(value: str) -> Tuple[int, ...]:
|
|
|
65
65
|
return tuple(ports)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def scan_ports(
|
|
69
|
-
|
|
68
|
+
def scan_ports(
|
|
69
|
+
ip: str,
|
|
70
|
+
ports: Iterable[int],
|
|
71
|
+
timeout: float = 1.0,
|
|
72
|
+
executor: Optional[concurrent.futures.Executor] = None,
|
|
73
|
+
) -> List[int]:
|
|
74
|
+
"""Probe ``ports`` on ``ip`` concurrently; return the ones that accepted a connection.
|
|
75
|
+
|
|
76
|
+
Pass ``executor`` to share one bounded pool across many hosts; otherwise a
|
|
77
|
+
pool with one worker per port is created for this call.
|
|
78
|
+
"""
|
|
70
79
|
ports = list(ports)
|
|
71
80
|
if not ports:
|
|
72
81
|
return []
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
futures[future]
|
|
78
|
-
for future in concurrent.futures.as_completed(futures)
|
|
79
|
-
if future.result()
|
|
80
|
-
]
|
|
83
|
+
if executor is None:
|
|
84
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(ports)) as own:
|
|
85
|
+
return scan_ports(ip, ports, timeout, executor=own)
|
|
81
86
|
|
|
87
|
+
futures = {executor.submit(_probe, ip, port, timeout): port for port in ports}
|
|
88
|
+
open_ports = [
|
|
89
|
+
futures[future] for future in concurrent.futures.as_completed(futures) if future.result()
|
|
90
|
+
]
|
|
82
91
|
return sorted(open_ports)
|