ipmg 2.2.0__tar.gz → 2.4.0__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.
Files changed (83) hide show
  1. {ipmg-2.2.0/src/ipmg.egg-info → ipmg-2.4.0}/PKG-INFO +157 -11
  2. {ipmg-2.2.0 → ipmg-2.4.0}/README.md +154 -10
  3. {ipmg-2.2.0 → ipmg-2.4.0}/pyproject.toml +8 -1
  4. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/__init__.py +1 -1
  5. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/cli/commands.py +22 -2
  6. ipmg-2.4.0/src/ipmg/cli/config.py +410 -0
  7. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/cli/parser.py +158 -7
  8. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/core/diff.py +6 -0
  9. ipmg-2.4.0/src/ipmg/core/health.py +64 -0
  10. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/exceptions.py +8 -0
  11. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/infrastructure/file_io.py +91 -5
  12. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/infrastructure/incremental.py +10 -0
  13. ipmg-2.4.0/src/ipmg/infrastructure/notify.py +403 -0
  14. ipmg-2.4.0/src/ipmg/reporting/machine.py +88 -0
  15. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/reporting/ui.py +13 -0
  16. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/services/scan_service.py +96 -17
  17. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/utils/helpers.py +10 -7
  18. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/app.py +229 -69
  19. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/manager.py +18 -16
  20. ipmg-2.4.0/src/ipmg/web/schemas.py +239 -0
  21. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/server.py +1 -1
  22. {ipmg-2.2.0 → ipmg-2.4.0/src/ipmg.egg-info}/PKG-INFO +157 -11
  23. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg.egg-info/SOURCES.txt +10 -0
  24. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg.egg-info/requires.txt +4 -0
  25. ipmg-2.4.0/tests/test_api_contract.py +153 -0
  26. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_commands.py +51 -1
  27. ipmg-2.4.0/tests/test_config.py +402 -0
  28. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_file_io.py +146 -2
  29. ipmg-2.4.0/tests/test_health.py +63 -0
  30. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_incremental.py +8 -7
  31. ipmg-2.4.0/tests/test_machine_output.py +147 -0
  32. ipmg-2.4.0/tests/test_notify.py +520 -0
  33. ipmg-2.4.0/tests/test_parser.py +157 -0
  34. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_scan_service.py +184 -10
  35. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_web_api.py +64 -0
  36. ipmg-2.2.0/tests/test_parser.py +0 -67
  37. {ipmg-2.2.0 → ipmg-2.4.0}/LICENSE +0 -0
  38. {ipmg-2.2.0 → ipmg-2.4.0}/setup.cfg +0 -0
  39. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/__main__.py +0 -0
  40. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/cli/__init__.py +0 -0
  41. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/core/__init__.py +0 -0
  42. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/core/discovery.py +0 -0
  43. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/core/engine.py +0 -0
  44. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/core/ping.py +0 -0
  45. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/core/portscan.py +0 -0
  46. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/core/security.py +0 -0
  47. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/infrastructure/__init__.py +0 -0
  48. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/infrastructure/database.py +0 -0
  49. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/reporting/__init__.py +0 -0
  50. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/reporting/diff_report.py +0 -0
  51. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/reporting/frames.py +0 -0
  52. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/reporting/live.py +0 -0
  53. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/reporting/summary.py +0 -0
  54. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/services/__init__.py +0 -0
  55. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/services/history_service.py +0 -0
  56. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/utils/__init__.py +0 -0
  57. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/__init__.py +0 -0
  58. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/db.py +0 -0
  59. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/static/css/app.css +0 -0
  60. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/static/index.html +0 -0
  61. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/static/js/api.js +0 -0
  62. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/static/js/app.js +0 -0
  63. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/static/js/charts.js +0 -0
  64. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/static/js/demo.js +0 -0
  65. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg/web/static/js/views.js +0 -0
  66. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg.egg-info/dependency_links.txt +0 -0
  67. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg.egg-info/entry_points.txt +0 -0
  68. {ipmg-2.2.0 → ipmg-2.4.0}/src/ipmg.egg-info/top_level.txt +0 -0
  69. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_database_history.py +0 -0
  70. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_diff.py +0 -0
  71. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_diff_report.py +0 -0
  72. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_discover.py +0 -0
  73. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_engine.py +0 -0
  74. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_history_service.py +0 -0
  75. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_live.py +0 -0
  76. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_ping.py +0 -0
  77. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_ping_command.py +0 -0
  78. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_portscan.py +0 -0
  79. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_ui.py +0 -0
  80. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_utils.py +0 -0
  81. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_web_db.py +0 -0
  82. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_web_manager.py +0 -0
  83. {ipmg-2.2.0 → ipmg-2.4.0}/tests/test_web_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipmg
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: IP Management & Ping Monitoring CLI Tool
5
5
  Author: Sameer Alam
6
6
  Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
@@ -33,9 +33,11 @@ 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
36
+ Requires-Dist: pydantic>=2.7
36
37
  Requires-Dist: uvicorn>=0.27
37
38
  Requires-Dist: websockets>=12
38
39
  Requires-Dist: python-multipart>=0.0.9
40
+ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
39
41
  Provides-Extra: dev
40
42
  Requires-Dist: pytest>=7.4; extra == "dev"
41
43
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
@@ -330,8 +332,10 @@ Or pick a ready-made command:
330
332
  | --- | --- |
331
333
  | Scan the network I am on | `ipmg --discover` |
332
334
  | Scan hosts listed in a file | `ipmg --input targets.txt` |
335
+ | Scan a file plus a few extra hosts | `ipmg --input targets.txt 10.0.0.0/30 10.0.0.5` |
333
336
  | Get names, not just IP addresses | `ipmg --input targets.txt --resolve` |
334
337
  | Get a report I can send to someone | `ipmg --input targets.txt --formats md csv` |
338
+ | Pipe the results into a script | `ipmg --input targets.txt --json \| jq .` |
335
339
  | See hosts appear as they answer | `ipmg --input 192.168.1.0/24 --stream` |
336
340
  | See what changed since last time | `ipmg --input targets.txt --compare` |
337
341
  | Check which services are listening | `ipmg --input targets.txt --scan-ports` |
@@ -390,6 +394,21 @@ ipmg diff --fail-on-change # exit 2 when anything changed (CI)
390
394
  ipmg history --limit 10 # list stored scans
391
395
  ```
392
396
 
397
+ To hear about changes without watching the terminal, send them to Slack,
398
+ Microsoft Teams, any JSON webhook, or email. Any `--notify-*` flag turns on
399
+ `--compare`, and with `--interval` every pass that changes something alerts:
400
+
401
+ ```bash
402
+ ipmg --input targets.txt --interval 15 --notify-slack # URL from $IPMG_NOTIFY_SLACK
403
+ ipmg --input targets.txt --notify-email ops@example.com --smtp-host smtp.example.com
404
+ ipmg diff --notify-webhook https://ops.example.com/ipmg --notify-severity critical
405
+ ```
406
+
407
+ Only changes at or above `--notify-severity` (default `warning`) trigger a
408
+ notification. A notification that fails is reported but never fails the scan.
409
+ Secrets such as webhook URLs and `IPMG_SMTP_PASSWORD` can come from environment
410
+ variables; see [Notifications](docs/COMMANDS.md#notifications) for all of them.
411
+
393
412
  What counts as a change:
394
413
 
395
414
  | Change | Severity | Meaning |
@@ -422,6 +441,11 @@ source**, so file-based and `--discover` runs do not get mixed up. Pass
422
441
  | `--latency-threshold` | `5` | Minimum latency delta in ms |
423
442
  | `--latency-pct` | `25` | Minimum relative latency change |
424
443
  | `--fail-on-change` | off | `ipmg diff` exits 2 when changes are found |
444
+ | `--notify-webhook` | off | POST the change report as JSON to a URL |
445
+ | `--notify-slack` | off | Post changes to a Slack incoming webhook |
446
+ | `--notify-teams` | off | Post changes to a Microsoft Teams Workflows webhook |
447
+ | `--notify-email` | off | Email the Markdown change report (with `--smtp-host`, `--smtp-port`, `--smtp-security`, `--smtp-user`, `--smtp-from`) |
448
+ | `--notify-severity` | `warning` | Only notify for changes at least this severe |
425
449
 
426
450
  ---
427
451
 
@@ -462,6 +486,14 @@ package, nothing is loaded from a CDN. It gives you:
462
486
  | `--no-browser` | off | Don't open the browser automatically |
463
487
  | `--db` | `~/.ipmg/dashboard.db` | History database location |
464
488
 
489
+ ### Scripting IPMG Web
490
+
491
+ Everything the dashboard does goes through a documented, versioned REST API
492
+ under `/api/v1`, so you can start scans, fetch results, and compare scans from
493
+ your own scripts. The [API guide](https://github.com/sameeralam3127/ipmg/blob/main/docs/API.md)
494
+ covers authentication, errors, and worked `curl` examples; the running server
495
+ also serves interactive docs at `http://127.0.0.1:8080/docs`.
496
+
465
497
  ### On a server with no browser
466
498
 
467
499
  On a Linux server with no display (e.g. accessed over plain SSH), IPMG detects
@@ -505,9 +537,32 @@ Anywhere IPMG takes `--input`, you can give it any of these:
505
537
  | 192.168.1.1 |
506
538
  | 10.0.1.0/30 |
507
539
 
508
- Duplicate targets are removed automatically, and one scan expands to at most
509
- 65,536 hosts — larger CIDR blocks or ranges are rejected up front, before the
510
- scan starts.
540
+ - **A JSON file** (`.json`) — a list of addresses, a list of objects keyed by
541
+ `IP Address`, `ip`, or `target`, or an object with a `targets` or `ips` array:
542
+
543
+ ```json
544
+ ["192.168.1.1", "10.0.1.0/30"]
545
+ ```
546
+
547
+ Because `IP Address` is one of the keys it accepts, a report IPMG wrote with
548
+ `--formats json` can be fed straight back in:
549
+
550
+ ```bash
551
+ ipmg --input results_20260628_120000.json
552
+ ```
553
+
554
+ `--input` takes as many of these as you like, in any mix, and may also be
555
+ repeated:
556
+
557
+ ```bash
558
+ ipmg --input targets.txt 10.0.0.0/30 10.0.0.5
559
+ ipmg --input targets.txt --input 10.0.0.5 # the same thing
560
+ ```
561
+
562
+ Duplicate targets are removed automatically — across sources too, so a host
563
+ that is both in the file and on the command line is scanned once — and one scan
564
+ expands to at most 65,536 hosts in total. Larger CIDR blocks, ranges, or
565
+ combinations are rejected up front, before the scan starts.
511
566
 
512
567
  ---
513
568
 
@@ -557,6 +612,22 @@ preferring `jsonl` or `csv` (current to the last host) over `json` or `xlsx`
557
612
  (current to the last autosave). Hosts dropped from the target list since are
558
613
  left out of the finished report.
559
614
 
615
+ **Piping results into a script.** `--json` prints the finished scan to stdout as
616
+ a JSON array, and `--jsonl` prints one object per host the moment its probe
617
+ finishes. The human output moves to stderr, so stdout is nothing but data and no
618
+ temp file or glob is needed:
619
+
620
+ ```bash
621
+ ipmg --input 10.0.0.0/24 --json | jq '.[] | select(.Status == "Active")'
622
+ ipmg --input 10.0.0.0/24 --jsonl | while read -r host; do notify "$host"; done
623
+ ipmg --input 10.0.0.0/24 --json 2>/dev/null > hosts.json # data only
624
+ ```
625
+
626
+ The field names are the report columns above, and both flags print a host
627
+ identically — `--json` is what `--jsonl` prints, gathered into an array. Unless
628
+ you ask for `--formats` explicitly, a piped scan writes no report file at all.
629
+ Exit codes are unchanged.
630
+
560
631
  **Open ports.** `Open Ports` is only populated when `--scan-ports` is set: for
561
632
  each host that answers, IPMG probes a list of common TCP ports (SSH, HTTP,
562
633
  HTTPS, RDP, SMB, FTP, SMTP, DNS, MSSQL, MySQL, PostgreSQL by default)
@@ -573,6 +644,61 @@ them — so piping IPMG into a file or a log gives you clean text.
573
644
 
574
645
  ---
575
646
 
647
+ ## Default options in a file
648
+
649
+ If every run repeats the same flags, put them in **`ipmg.toml`** next to your
650
+ work instead:
651
+
652
+ ```toml
653
+ threads = 200
654
+ timeout = 1
655
+ resolve = true
656
+ formats = ["md", "csv"]
657
+
658
+ [profile.datacenter]
659
+ threads = 400
660
+ scan-ports = true
661
+ ports = "22,80,443"
662
+ ```
663
+
664
+ ```bash
665
+ ipmg --input targets.txt # uses the file's defaults
666
+ ipmg --input targets.txt --profile datacenter # and the profile on top
667
+ ipmg --input targets.txt --threads 20 # a flag always wins
668
+ ```
669
+
670
+ Any long flag can be a key, written as the flag is (`scan-ports`) or with
671
+ underscores (`scan_ports`). A switch takes `true` to mean "as if the flag were
672
+ passed" — `no-history = true` is `--no-history`. Files are read from
673
+ `~/.config/ipmg/config.toml` first (or `$XDG_CONFIG_HOME`), then `./ipmg.toml`
674
+ on top, so a project can override your global defaults:
675
+
676
+ | Flag | What it does |
677
+ | --- | --- |
678
+ | `--config PATH` | Read that file instead of searching |
679
+ | `--no-config` | Ignore every file and use the built-in defaults |
680
+ | `--profile NAME` | Apply the `[profile.NAME]` section as well |
681
+
682
+ **The command line always wins.** A flag that appears on it ignores the file
683
+ entirely for that flag — including `--input`, which merges several sources on
684
+ one command line but replaces the file's list rather than adding to it.
685
+
686
+ A key that is not a flag, a value of the wrong type, or a value outside a
687
+ flag's choices is an error naming the key and the file it came from:
688
+
689
+ ```text
690
+ ✗ Unknown option 'thredas' in ipmg.toml. Did you mean 'threads'?
691
+ ✗ 'formats' in ipmg.toml: 'pdf' is not one of xlsx, csv, json, jsonl, md.
692
+ ```
693
+
694
+ A project's `./ipmg.toml` comes with whatever directory you scan from,
695
+ including a repository you just cloned, so it cannot say where results are
696
+ sent: the `--notify-*` destinations and `--smtp-*` settings are only read from
697
+ `~/.config/ipmg/config.toml` or a file you pass with `--config`. A file also
698
+ cannot combine flags that exclude each other, such as `json` and `jsonl`.
699
+
700
+ ---
701
+
576
702
  ## All options
577
703
 
578
704
  `ipmg --help` always lists the current set. Grouped for reading:
@@ -581,10 +707,12 @@ them — so piping IPMG into a file or a log gives you clean text.
581
707
 
582
708
  | Flag | Default | Description |
583
709
  | --- | --- | --- |
584
- | `--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`) |
710
+ | `--input` | `ip_list.xlsx` | What to scan: one or more files (`.xlsx`, `.xls`, `.csv`, `.json`, `.txt`, `.list`), IPs, CIDR blocks, or ranges (`10.0.0.1-10.0.0.50`), merged and de-duplicated |
585
711
  | `--discover` | off | Auto-detect and scan the local subnet instead |
586
712
  | `--output` | `results` | Report file name prefix |
587
- | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` |
713
+ | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` (no file at all with `--json`/`--jsonl`) |
714
+ | `--json` | off | Print the finished scan to stdout as a JSON array, human output on stderr |
715
+ | `--jsonl` | off | Stream one JSON object per host to stdout as each probe finishes |
588
716
  | `--no-incremental` | off | Only write the report once the scan has finished |
589
717
  | `--autosave` | `30` | How often a running scan re-saves `xlsx`, `json`, and `md` |
590
718
  | `--resume` | off | Finish an interrupted scan from its partial report (newest one, or the path given) |
@@ -616,13 +744,25 @@ them — so piping IPMG into a file or a log gives you clean text.
616
744
  | `--stream-all` | off | Stream every result, including hosts that did not answer (implies `--stream`) |
617
745
  | `--stream-refresh` | `0.25` | Seconds between progress-bar redraws while streaming (0.05-5) |
618
746
  | `--verbose` | off | Debug logging |
747
+ | `--config` | search | Read defaults from this file instead of searching for `ipmg.toml` |
748
+ | `--no-config` | off | Ignore every configuration file |
749
+ | `--profile` | none | Apply a `[profile.NAME]` section from the configuration file |
750
+
751
+ **Exit status**
752
+
753
+ | Flag | Default | Description |
754
+ | --- | --- | --- |
755
+ | `--fail-on-down` | off | Exit 3 if any target is not `Active` (reports are still written) |
756
+ | `--min-active` | off | Exit 3 if fewer than this percentage of targets are `Active` |
619
757
 
620
758
  **History and changes** — see [Change detection](#change-detection) for
621
759
  `--compare`, `--no-history`, `--db`, `--diff-formats`, `--diff-output`,
622
- `--latency-threshold`, `--latency-pct`, and `--fail-on-change`.
760
+ `--latency-threshold`, `--latency-pct`, `--fail-on-change`, and the
761
+ `--notify-*` flags.
623
762
 
624
763
  Exit codes: `0` success, `1` error, `2` changes detected
625
- (`ipmg diff --fail-on-change`), `130` interrupted.
764
+ (`ipmg diff --fail-on-change`), `3` hosts down (`--fail-on-down`,
765
+ `--min-active`), `130` interrupted.
626
766
 
627
767
  ---
628
768
 
@@ -643,12 +783,15 @@ itself is hardened accordingly:
643
783
  - Uploads are capped at 5 MB and one scan expands to at most 65,536 hosts,
644
784
  so a bad input file cannot exhaust memory
645
785
  - All database access uses parameterized SQL
786
+ - The only outbound requests IPMG makes are the notifications you ask for
787
+ with a `--notify-*` flag. Their URLs and the SMTP password can come from
788
+ environment variables, and error messages never repeat them
646
789
 
647
790
  If you bind to a non-local address with `--host`, the token still guards the
648
791
  API. It travels over plain HTTP, though, so use an SSH tunnel or a reverse
649
792
  proxy with TLS on any network you don't trust.
650
793
 
651
- Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
794
+ Found a vulnerability? See [SECURITY.md](.github/SECURITY.md) for how to report it.
652
795
 
653
796
  ---
654
797
 
@@ -657,6 +800,9 @@ Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
657
800
  - **[Command reference](https://github.com/sameeralam3127/ipmg/blob/main/docs/COMMANDS.md)** —
658
801
  every command and flag with copy-paste examples, a safe session that tries
659
802
  everything on your own machine, exit codes, and error messages
803
+ - **[Web API guide](https://github.com/sameeralam3127/ipmg/blob/main/docs/API.md)** —
804
+ script IPMG Web over HTTP: start scans, fetch results, compare scans,
805
+ and follow live events
660
806
  - **[Troubleshooting](https://github.com/sameeralam3127/ipmg/blob/main/docs/TROUBLESHOOTING.md)** —
661
807
  install errors, `command not found`, every host timing out, slow scans,
662
808
  rejected input files
@@ -670,11 +816,11 @@ Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
670
816
  ## Contributing
671
817
 
672
818
  Contributions are welcome.
673
- [CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/CONTRIBUTING.md)
819
+ [CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/.github/CONTRIBUTING.md)
674
820
  covers setting up a development environment, running the tests, the commit
675
821
  message format that drives automated releases, and how the project website and
676
822
  IPMG Web demo are built. Everyone taking part is expected to follow the
677
- [Code of Conduct](https://github.com/sameeralam3127/ipmg/blob/main/CODE_OF_CONDUCT.md).
823
+ [Code of Conduct](https://github.com/sameeralam3127/ipmg/blob/main/.github/CODE_OF_CONDUCT.md).
678
824
 
679
825
  ---
680
826
 
@@ -283,8 +283,10 @@ Or pick a ready-made command:
283
283
  | --- | --- |
284
284
  | Scan the network I am on | `ipmg --discover` |
285
285
  | Scan hosts listed in a file | `ipmg --input targets.txt` |
286
+ | Scan a file plus a few extra hosts | `ipmg --input targets.txt 10.0.0.0/30 10.0.0.5` |
286
287
  | Get names, not just IP addresses | `ipmg --input targets.txt --resolve` |
287
288
  | Get a report I can send to someone | `ipmg --input targets.txt --formats md csv` |
289
+ | Pipe the results into a script | `ipmg --input targets.txt --json \| jq .` |
288
290
  | See hosts appear as they answer | `ipmg --input 192.168.1.0/24 --stream` |
289
291
  | See what changed since last time | `ipmg --input targets.txt --compare` |
290
292
  | Check which services are listening | `ipmg --input targets.txt --scan-ports` |
@@ -343,6 +345,21 @@ ipmg diff --fail-on-change # exit 2 when anything changed (CI)
343
345
  ipmg history --limit 10 # list stored scans
344
346
  ```
345
347
 
348
+ To hear about changes without watching the terminal, send them to Slack,
349
+ Microsoft Teams, any JSON webhook, or email. Any `--notify-*` flag turns on
350
+ `--compare`, and with `--interval` every pass that changes something alerts:
351
+
352
+ ```bash
353
+ ipmg --input targets.txt --interval 15 --notify-slack # URL from $IPMG_NOTIFY_SLACK
354
+ ipmg --input targets.txt --notify-email ops@example.com --smtp-host smtp.example.com
355
+ ipmg diff --notify-webhook https://ops.example.com/ipmg --notify-severity critical
356
+ ```
357
+
358
+ Only changes at or above `--notify-severity` (default `warning`) trigger a
359
+ notification. A notification that fails is reported but never fails the scan.
360
+ Secrets such as webhook URLs and `IPMG_SMTP_PASSWORD` can come from environment
361
+ variables; see [Notifications](docs/COMMANDS.md#notifications) for all of them.
362
+
346
363
  What counts as a change:
347
364
 
348
365
  | Change | Severity | Meaning |
@@ -375,6 +392,11 @@ source**, so file-based and `--discover` runs do not get mixed up. Pass
375
392
  | `--latency-threshold` | `5` | Minimum latency delta in ms |
376
393
  | `--latency-pct` | `25` | Minimum relative latency change |
377
394
  | `--fail-on-change` | off | `ipmg diff` exits 2 when changes are found |
395
+ | `--notify-webhook` | off | POST the change report as JSON to a URL |
396
+ | `--notify-slack` | off | Post changes to a Slack incoming webhook |
397
+ | `--notify-teams` | off | Post changes to a Microsoft Teams Workflows webhook |
398
+ | `--notify-email` | off | Email the Markdown change report (with `--smtp-host`, `--smtp-port`, `--smtp-security`, `--smtp-user`, `--smtp-from`) |
399
+ | `--notify-severity` | `warning` | Only notify for changes at least this severe |
378
400
 
379
401
  ---
380
402
 
@@ -415,6 +437,14 @@ package, nothing is loaded from a CDN. It gives you:
415
437
  | `--no-browser` | off | Don't open the browser automatically |
416
438
  | `--db` | `~/.ipmg/dashboard.db` | History database location |
417
439
 
440
+ ### Scripting IPMG Web
441
+
442
+ Everything the dashboard does goes through a documented, versioned REST API
443
+ under `/api/v1`, so you can start scans, fetch results, and compare scans from
444
+ your own scripts. The [API guide](https://github.com/sameeralam3127/ipmg/blob/main/docs/API.md)
445
+ covers authentication, errors, and worked `curl` examples; the running server
446
+ also serves interactive docs at `http://127.0.0.1:8080/docs`.
447
+
418
448
  ### On a server with no browser
419
449
 
420
450
  On a Linux server with no display (e.g. accessed over plain SSH), IPMG detects
@@ -458,9 +488,32 @@ Anywhere IPMG takes `--input`, you can give it any of these:
458
488
  | 192.168.1.1 |
459
489
  | 10.0.1.0/30 |
460
490
 
461
- Duplicate targets are removed automatically, and one scan expands to at most
462
- 65,536 hosts — larger CIDR blocks or ranges are rejected up front, before the
463
- scan starts.
491
+ - **A JSON file** (`.json`) — a list of addresses, a list of objects keyed by
492
+ `IP Address`, `ip`, or `target`, or an object with a `targets` or `ips` array:
493
+
494
+ ```json
495
+ ["192.168.1.1", "10.0.1.0/30"]
496
+ ```
497
+
498
+ Because `IP Address` is one of the keys it accepts, a report IPMG wrote with
499
+ `--formats json` can be fed straight back in:
500
+
501
+ ```bash
502
+ ipmg --input results_20260628_120000.json
503
+ ```
504
+
505
+ `--input` takes as many of these as you like, in any mix, and may also be
506
+ repeated:
507
+
508
+ ```bash
509
+ ipmg --input targets.txt 10.0.0.0/30 10.0.0.5
510
+ ipmg --input targets.txt --input 10.0.0.5 # the same thing
511
+ ```
512
+
513
+ Duplicate targets are removed automatically — across sources too, so a host
514
+ that is both in the file and on the command line is scanned once — and one scan
515
+ expands to at most 65,536 hosts in total. Larger CIDR blocks, ranges, or
516
+ combinations are rejected up front, before the scan starts.
464
517
 
465
518
  ---
466
519
 
@@ -510,6 +563,22 @@ preferring `jsonl` or `csv` (current to the last host) over `json` or `xlsx`
510
563
  (current to the last autosave). Hosts dropped from the target list since are
511
564
  left out of the finished report.
512
565
 
566
+ **Piping results into a script.** `--json` prints the finished scan to stdout as
567
+ a JSON array, and `--jsonl` prints one object per host the moment its probe
568
+ finishes. The human output moves to stderr, so stdout is nothing but data and no
569
+ temp file or glob is needed:
570
+
571
+ ```bash
572
+ ipmg --input 10.0.0.0/24 --json | jq '.[] | select(.Status == "Active")'
573
+ ipmg --input 10.0.0.0/24 --jsonl | while read -r host; do notify "$host"; done
574
+ ipmg --input 10.0.0.0/24 --json 2>/dev/null > hosts.json # data only
575
+ ```
576
+
577
+ The field names are the report columns above, and both flags print a host
578
+ identically — `--json` is what `--jsonl` prints, gathered into an array. Unless
579
+ you ask for `--formats` explicitly, a piped scan writes no report file at all.
580
+ Exit codes are unchanged.
581
+
513
582
  **Open ports.** `Open Ports` is only populated when `--scan-ports` is set: for
514
583
  each host that answers, IPMG probes a list of common TCP ports (SSH, HTTP,
515
584
  HTTPS, RDP, SMB, FTP, SMTP, DNS, MSSQL, MySQL, PostgreSQL by default)
@@ -526,6 +595,61 @@ them — so piping IPMG into a file or a log gives you clean text.
526
595
 
527
596
  ---
528
597
 
598
+ ## Default options in a file
599
+
600
+ If every run repeats the same flags, put them in **`ipmg.toml`** next to your
601
+ work instead:
602
+
603
+ ```toml
604
+ threads = 200
605
+ timeout = 1
606
+ resolve = true
607
+ formats = ["md", "csv"]
608
+
609
+ [profile.datacenter]
610
+ threads = 400
611
+ scan-ports = true
612
+ ports = "22,80,443"
613
+ ```
614
+
615
+ ```bash
616
+ ipmg --input targets.txt # uses the file's defaults
617
+ ipmg --input targets.txt --profile datacenter # and the profile on top
618
+ ipmg --input targets.txt --threads 20 # a flag always wins
619
+ ```
620
+
621
+ Any long flag can be a key, written as the flag is (`scan-ports`) or with
622
+ underscores (`scan_ports`). A switch takes `true` to mean "as if the flag were
623
+ passed" — `no-history = true` is `--no-history`. Files are read from
624
+ `~/.config/ipmg/config.toml` first (or `$XDG_CONFIG_HOME`), then `./ipmg.toml`
625
+ on top, so a project can override your global defaults:
626
+
627
+ | Flag | What it does |
628
+ | --- | --- |
629
+ | `--config PATH` | Read that file instead of searching |
630
+ | `--no-config` | Ignore every file and use the built-in defaults |
631
+ | `--profile NAME` | Apply the `[profile.NAME]` section as well |
632
+
633
+ **The command line always wins.** A flag that appears on it ignores the file
634
+ entirely for that flag — including `--input`, which merges several sources on
635
+ one command line but replaces the file's list rather than adding to it.
636
+
637
+ A key that is not a flag, a value of the wrong type, or a value outside a
638
+ flag's choices is an error naming the key and the file it came from:
639
+
640
+ ```text
641
+ ✗ Unknown option 'thredas' in ipmg.toml. Did you mean 'threads'?
642
+ ✗ 'formats' in ipmg.toml: 'pdf' is not one of xlsx, csv, json, jsonl, md.
643
+ ```
644
+
645
+ A project's `./ipmg.toml` comes with whatever directory you scan from,
646
+ including a repository you just cloned, so it cannot say where results are
647
+ sent: the `--notify-*` destinations and `--smtp-*` settings are only read from
648
+ `~/.config/ipmg/config.toml` or a file you pass with `--config`. A file also
649
+ cannot combine flags that exclude each other, such as `json` and `jsonl`.
650
+
651
+ ---
652
+
529
653
  ## All options
530
654
 
531
655
  `ipmg --help` always lists the current set. Grouped for reading:
@@ -534,10 +658,12 @@ them — so piping IPMG into a file or a log gives you clean text.
534
658
 
535
659
  | Flag | Default | Description |
536
660
  | --- | --- | --- |
537
- | `--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`) |
661
+ | `--input` | `ip_list.xlsx` | What to scan: one or more files (`.xlsx`, `.xls`, `.csv`, `.json`, `.txt`, `.list`), IPs, CIDR blocks, or ranges (`10.0.0.1-10.0.0.50`), merged and de-duplicated |
538
662
  | `--discover` | off | Auto-detect and scan the local subnet instead |
539
663
  | `--output` | `results` | Report file name prefix |
540
- | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` |
664
+ | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` (no file at all with `--json`/`--jsonl`) |
665
+ | `--json` | off | Print the finished scan to stdout as a JSON array, human output on stderr |
666
+ | `--jsonl` | off | Stream one JSON object per host to stdout as each probe finishes |
541
667
  | `--no-incremental` | off | Only write the report once the scan has finished |
542
668
  | `--autosave` | `30` | How often a running scan re-saves `xlsx`, `json`, and `md` |
543
669
  | `--resume` | off | Finish an interrupted scan from its partial report (newest one, or the path given) |
@@ -569,13 +695,25 @@ them — so piping IPMG into a file or a log gives you clean text.
569
695
  | `--stream-all` | off | Stream every result, including hosts that did not answer (implies `--stream`) |
570
696
  | `--stream-refresh` | `0.25` | Seconds between progress-bar redraws while streaming (0.05-5) |
571
697
  | `--verbose` | off | Debug logging |
698
+ | `--config` | search | Read defaults from this file instead of searching for `ipmg.toml` |
699
+ | `--no-config` | off | Ignore every configuration file |
700
+ | `--profile` | none | Apply a `[profile.NAME]` section from the configuration file |
701
+
702
+ **Exit status**
703
+
704
+ | Flag | Default | Description |
705
+ | --- | --- | --- |
706
+ | `--fail-on-down` | off | Exit 3 if any target is not `Active` (reports are still written) |
707
+ | `--min-active` | off | Exit 3 if fewer than this percentage of targets are `Active` |
572
708
 
573
709
  **History and changes** — see [Change detection](#change-detection) for
574
710
  `--compare`, `--no-history`, `--db`, `--diff-formats`, `--diff-output`,
575
- `--latency-threshold`, `--latency-pct`, and `--fail-on-change`.
711
+ `--latency-threshold`, `--latency-pct`, `--fail-on-change`, and the
712
+ `--notify-*` flags.
576
713
 
577
714
  Exit codes: `0` success, `1` error, `2` changes detected
578
- (`ipmg diff --fail-on-change`), `130` interrupted.
715
+ (`ipmg diff --fail-on-change`), `3` hosts down (`--fail-on-down`,
716
+ `--min-active`), `130` interrupted.
579
717
 
580
718
  ---
581
719
 
@@ -596,12 +734,15 @@ itself is hardened accordingly:
596
734
  - Uploads are capped at 5 MB and one scan expands to at most 65,536 hosts,
597
735
  so a bad input file cannot exhaust memory
598
736
  - All database access uses parameterized SQL
737
+ - The only outbound requests IPMG makes are the notifications you ask for
738
+ with a `--notify-*` flag. Their URLs and the SMTP password can come from
739
+ environment variables, and error messages never repeat them
599
740
 
600
741
  If you bind to a non-local address with `--host`, the token still guards the
601
742
  API. It travels over plain HTTP, though, so use an SSH tunnel or a reverse
602
743
  proxy with TLS on any network you don't trust.
603
744
 
604
- Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
745
+ Found a vulnerability? See [SECURITY.md](.github/SECURITY.md) for how to report it.
605
746
 
606
747
  ---
607
748
 
@@ -610,6 +751,9 @@ Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
610
751
  - **[Command reference](https://github.com/sameeralam3127/ipmg/blob/main/docs/COMMANDS.md)** —
611
752
  every command and flag with copy-paste examples, a safe session that tries
612
753
  everything on your own machine, exit codes, and error messages
754
+ - **[Web API guide](https://github.com/sameeralam3127/ipmg/blob/main/docs/API.md)** —
755
+ script IPMG Web over HTTP: start scans, fetch results, compare scans,
756
+ and follow live events
613
757
  - **[Troubleshooting](https://github.com/sameeralam3127/ipmg/blob/main/docs/TROUBLESHOOTING.md)** —
614
758
  install errors, `command not found`, every host timing out, slow scans,
615
759
  rejected input files
@@ -623,11 +767,11 @@ Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
623
767
  ## Contributing
624
768
 
625
769
  Contributions are welcome.
626
- [CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/CONTRIBUTING.md)
770
+ [CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/.github/CONTRIBUTING.md)
627
771
  covers setting up a development environment, running the tests, the commit
628
772
  message format that drives automated releases, and how the project website and
629
773
  IPMG Web demo are built. Everyone taking part is expected to follow the
630
- [Code of Conduct](https://github.com/sameeralam3127/ipmg/blob/main/CODE_OF_CONDUCT.md).
774
+ [Code of Conduct](https://github.com/sameeralam3127/ipmg/blob/main/.github/CODE_OF_CONDUCT.md).
631
775
 
632
776
  ---
633
777
 
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
13
13
 
14
14
  [project]
15
15
  name = "ipmg"
16
- version = "2.2.0" # Managed automatically by semantic-release
16
+ version = "2.4.0" # 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"
@@ -53,9 +53,13 @@ dependencies = [
53
53
  "openpyxl>=3.1",
54
54
  "rich>=13.0",
55
55
  "fastapi>=0.110",
56
+ "pydantic>=2.7",
56
57
  "uvicorn>=0.27",
57
58
  "websockets>=12",
58
59
  "python-multipart>=0.0.9",
60
+ # tomllib is in the standard library from 3.11; tomli 2.x is the same
61
+ # parser with the same API, so the floor must not drop below it.
62
+ "tomli>=2.0.1; python_version < '3.11'",
59
63
  ]
60
64
 
61
65
  [project.urls]
@@ -109,6 +113,9 @@ commit_message = "chore(release): {version}"
109
113
  build_command = "python -m build"
110
114
  major_on_zero = false
111
115
 
116
+ [tool.semantic_release.changelog.default_templates]
117
+ changelog_file = ".github/CHANGELOG.md"
118
+
112
119
 
113
120
  # ==========================================================
114
121
  # Setuptools (src layout)
@@ -2,4 +2,4 @@
2
2
  ipmg - IP Management & Ping Monitoring Tool
3
3
  """
4
4
 
5
- __version__ = "2.2.0"
5
+ __version__ = "2.4.0"
@@ -6,6 +6,7 @@ import logging
6
6
  import sys
7
7
  from typing import Callable, Dict, List, Optional
8
8
 
9
+ from ipmg.cli import config
9
10
  from ipmg.cli.parser import (
10
11
  build_diff_parser,
11
12
  build_history_parser,
@@ -13,26 +14,37 @@ from ipmg.cli.parser import (
13
14
  build_web_parser,
14
15
  )
15
16
  from ipmg.core.diff import DiffOptions
17
+ from ipmg.core.health import HostsDownError
16
18
  from ipmg.core.security import print_disclaimer_once
17
19
  from ipmg.exceptions import IPMGError
20
+ from ipmg.infrastructure.notify import notify_options, send_notifications
18
21
  from ipmg.reporting import ui
19
22
  from ipmg.reporting.diff_report import export_diff, print_diff
20
23
  from ipmg.reporting.summary import print_scan_history
21
24
  from ipmg.services.history_service import HistoryService
22
- from ipmg.services.scan_service import run_scan
25
+ from ipmg.services.scan_service import machine_output, run_scan
23
26
  from ipmg.utils.helpers import configure_logging
24
27
 
25
28
  EXIT_OK = 0
26
29
  EXIT_ERROR = 1
27
30
  EXIT_CHANGES_DETECTED = 2
31
+ EXIT_HOSTS_DOWN = 3
28
32
  EXIT_INTERRUPTED = 130
29
33
 
30
34
  log = logging.getLogger(__name__)
31
35
 
32
36
 
33
37
  def _scan_command(argv: List[str]) -> int:
34
- args = build_parser().parse_args(argv)
38
+ parser = build_parser()
39
+ # Before parsing: the file supplies the defaults, so anything on the
40
+ # command line still overrides it.
41
+ config.apply(parser, build_parser, argv)
42
+ args = parser.parse_args(argv)
35
43
  configure_logging(args.verbose)
44
+ if machine_output(args).enabled:
45
+ # Before the banner: with --json or --jsonl stdout carries the results
46
+ # and nothing else, so every line a person reads goes to stderr.
47
+ ui.use_stderr()
36
48
  ui.header("scan")
37
49
  print_disclaimer_once()
38
50
  run_scan(args)
@@ -77,6 +89,7 @@ def _diff_command(argv: List[str]) -> int:
77
89
  ui.error("Provide at most two scan ids: BASELINE TARGET.")
78
90
  return EXIT_ERROR
79
91
 
92
+ notify = notify_options(args)
80
93
  history = HistoryService.open(args.db)
81
94
  options = DiffOptions(
82
95
  latency_abs_ms=max(args.latency_threshold, 0.0),
@@ -93,6 +106,7 @@ def _diff_command(argv: List[str]) -> int:
93
106
  print_diff(diff, limit=args.limit)
94
107
  if args.diff_formats:
95
108
  export_diff(diff, args.diff_output, args.diff_formats)
109
+ send_notifications(diff, notify)
96
110
 
97
111
  if args.fail_on_change and diff.has_changes:
98
112
  return EXIT_CHANGES_DETECTED
@@ -134,6 +148,12 @@ def run(argv: Optional[List[str]] = None) -> int:
134
148
 
135
149
  try:
136
150
  return handler(handler_argv)
151
+ except HostsDownError as exc:
152
+ # Not an error: the scan finished and wrote its reports, but failed
153
+ # the check it was asked to make.
154
+ ui.blank()
155
+ ui.warn(str(exc))
156
+ return EXIT_HOSTS_DOWN
137
157
  except IPMGError as exc:
138
158
  ui.blank()
139
159
  ui.error(str(exc))