ipmg 2.2.0__tar.gz → 2.3.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.
- {ipmg-2.2.0/src/ipmg.egg-info → ipmg-2.3.0}/PKG-INFO +50 -6
- {ipmg-2.2.0 → ipmg-2.3.0}/README.md +48 -5
- {ipmg-2.2.0 → ipmg-2.3.0}/pyproject.toml +5 -1
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/__init__.py +1 -1
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/cli/commands.py +11 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/cli/parser.py +118 -1
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/core/diff.py +6 -0
- ipmg-2.3.0/src/ipmg/core/health.py +64 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/exceptions.py +4 -0
- ipmg-2.3.0/src/ipmg/infrastructure/notify.py +403 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/services/scan_service.py +27 -1
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/app.py +218 -39
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/manager.py +18 -16
- ipmg-2.3.0/src/ipmg/web/schemas.py +239 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/server.py +1 -1
- {ipmg-2.2.0 → ipmg-2.3.0/src/ipmg.egg-info}/PKG-INFO +50 -6
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg.egg-info/SOURCES.txt +6 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg.egg-info/requires.txt +1 -0
- ipmg-2.3.0/tests/test_api_contract.py +153 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_commands.py +50 -0
- ipmg-2.3.0/tests/test_health.py +63 -0
- ipmg-2.3.0/tests/test_notify.py +520 -0
- ipmg-2.3.0/tests/test_parser.py +140 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_scan_service.py +140 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_web_api.py +54 -0
- ipmg-2.2.0/tests/test_parser.py +0 -67
- {ipmg-2.2.0 → ipmg-2.3.0}/LICENSE +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/setup.cfg +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/__main__.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/cli/__init__.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/core/__init__.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/core/discovery.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/core/engine.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/core/ping.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/core/portscan.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/core/security.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/infrastructure/__init__.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/infrastructure/database.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/infrastructure/file_io.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/infrastructure/incremental.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/reporting/__init__.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/reporting/diff_report.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/reporting/frames.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/reporting/live.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/reporting/summary.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/reporting/ui.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/services/__init__.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/services/history_service.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/utils/__init__.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/utils/helpers.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/__init__.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/db.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/static/css/app.css +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/static/index.html +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/static/js/api.js +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/static/js/app.js +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/static/js/charts.js +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/static/js/demo.js +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg/web/static/js/views.js +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg.egg-info/dependency_links.txt +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg.egg-info/entry_points.txt +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/src/ipmg.egg-info/top_level.txt +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_database_history.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_diff.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_diff_report.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_discover.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_engine.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_file_io.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_history_service.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_incremental.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_live.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_ping.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_ping_command.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_portscan.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_ui.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_utils.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_web_db.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.0}/tests/test_web_manager.py +0 -0
- {ipmg-2.2.0 → ipmg-2.3.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.
|
|
3
|
+
Version: 2.3.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,6 +33,7 @@ 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
|
|
@@ -390,6 +391,21 @@ ipmg diff --fail-on-change # exit 2 when anything changed (CI)
|
|
|
390
391
|
ipmg history --limit 10 # list stored scans
|
|
391
392
|
```
|
|
392
393
|
|
|
394
|
+
To hear about changes without watching the terminal, send them to Slack,
|
|
395
|
+
Microsoft Teams, any JSON webhook, or email. Any `--notify-*` flag turns on
|
|
396
|
+
`--compare`, and with `--interval` every pass that changes something alerts:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
ipmg --input targets.txt --interval 15 --notify-slack # URL from $IPMG_NOTIFY_SLACK
|
|
400
|
+
ipmg --input targets.txt --notify-email ops@example.com --smtp-host smtp.example.com
|
|
401
|
+
ipmg diff --notify-webhook https://ops.example.com/ipmg --notify-severity critical
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Only changes at or above `--notify-severity` (default `warning`) trigger a
|
|
405
|
+
notification. A notification that fails is reported but never fails the scan.
|
|
406
|
+
Secrets such as webhook URLs and `IPMG_SMTP_PASSWORD` can come from environment
|
|
407
|
+
variables; see [Notifications](docs/COMMANDS.md#notifications) for all of them.
|
|
408
|
+
|
|
393
409
|
What counts as a change:
|
|
394
410
|
|
|
395
411
|
| Change | Severity | Meaning |
|
|
@@ -422,6 +438,11 @@ source**, so file-based and `--discover` runs do not get mixed up. Pass
|
|
|
422
438
|
| `--latency-threshold` | `5` | Minimum latency delta in ms |
|
|
423
439
|
| `--latency-pct` | `25` | Minimum relative latency change |
|
|
424
440
|
| `--fail-on-change` | off | `ipmg diff` exits 2 when changes are found |
|
|
441
|
+
| `--notify-webhook` | off | POST the change report as JSON to a URL |
|
|
442
|
+
| `--notify-slack` | off | Post changes to a Slack incoming webhook |
|
|
443
|
+
| `--notify-teams` | off | Post changes to a Microsoft Teams Workflows webhook |
|
|
444
|
+
| `--notify-email` | off | Email the Markdown change report (with `--smtp-host`, `--smtp-port`, `--smtp-security`, `--smtp-user`, `--smtp-from`) |
|
|
445
|
+
| `--notify-severity` | `warning` | Only notify for changes at least this severe |
|
|
425
446
|
|
|
426
447
|
---
|
|
427
448
|
|
|
@@ -462,6 +483,14 @@ package, nothing is loaded from a CDN. It gives you:
|
|
|
462
483
|
| `--no-browser` | off | Don't open the browser automatically |
|
|
463
484
|
| `--db` | `~/.ipmg/dashboard.db` | History database location |
|
|
464
485
|
|
|
486
|
+
### Scripting IPMG Web
|
|
487
|
+
|
|
488
|
+
Everything the dashboard does goes through a documented, versioned REST API
|
|
489
|
+
under `/api/v1`, so you can start scans, fetch results, and compare scans from
|
|
490
|
+
your own scripts. The [API guide](https://github.com/sameeralam3127/ipmg/blob/main/docs/API.md)
|
|
491
|
+
covers authentication, errors, and worked `curl` examples; the running server
|
|
492
|
+
also serves interactive docs at `http://127.0.0.1:8080/docs`.
|
|
493
|
+
|
|
465
494
|
### On a server with no browser
|
|
466
495
|
|
|
467
496
|
On a Linux server with no display (e.g. accessed over plain SSH), IPMG detects
|
|
@@ -617,12 +646,21 @@ them — so piping IPMG into a file or a log gives you clean text.
|
|
|
617
646
|
| `--stream-refresh` | `0.25` | Seconds between progress-bar redraws while streaming (0.05-5) |
|
|
618
647
|
| `--verbose` | off | Debug logging |
|
|
619
648
|
|
|
649
|
+
**Exit status**
|
|
650
|
+
|
|
651
|
+
| Flag | Default | Description |
|
|
652
|
+
| --- | --- | --- |
|
|
653
|
+
| `--fail-on-down` | off | Exit 3 if any target is not `Active` (reports are still written) |
|
|
654
|
+
| `--min-active` | off | Exit 3 if fewer than this percentage of targets are `Active` |
|
|
655
|
+
|
|
620
656
|
**History and changes** — see [Change detection](#change-detection) for
|
|
621
657
|
`--compare`, `--no-history`, `--db`, `--diff-formats`, `--diff-output`,
|
|
622
|
-
`--latency-threshold`, `--latency-pct`,
|
|
658
|
+
`--latency-threshold`, `--latency-pct`, `--fail-on-change`, and the
|
|
659
|
+
`--notify-*` flags.
|
|
623
660
|
|
|
624
661
|
Exit codes: `0` success, `1` error, `2` changes detected
|
|
625
|
-
(`ipmg diff --fail-on-change`), `
|
|
662
|
+
(`ipmg diff --fail-on-change`), `3` hosts down (`--fail-on-down`,
|
|
663
|
+
`--min-active`), `130` interrupted.
|
|
626
664
|
|
|
627
665
|
---
|
|
628
666
|
|
|
@@ -643,12 +681,15 @@ itself is hardened accordingly:
|
|
|
643
681
|
- Uploads are capped at 5 MB and one scan expands to at most 65,536 hosts,
|
|
644
682
|
so a bad input file cannot exhaust memory
|
|
645
683
|
- All database access uses parameterized SQL
|
|
684
|
+
- The only outbound requests IPMG makes are the notifications you ask for
|
|
685
|
+
with a `--notify-*` flag. Their URLs and the SMTP password can come from
|
|
686
|
+
environment variables, and error messages never repeat them
|
|
646
687
|
|
|
647
688
|
If you bind to a non-local address with `--host`, the token still guards the
|
|
648
689
|
API. It travels over plain HTTP, though, so use an SSH tunnel or a reverse
|
|
649
690
|
proxy with TLS on any network you don't trust.
|
|
650
691
|
|
|
651
|
-
Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
|
|
692
|
+
Found a vulnerability? See [SECURITY.md](.github/SECURITY.md) for how to report it.
|
|
652
693
|
|
|
653
694
|
---
|
|
654
695
|
|
|
@@ -657,6 +698,9 @@ Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
|
|
|
657
698
|
- **[Command reference](https://github.com/sameeralam3127/ipmg/blob/main/docs/COMMANDS.md)** —
|
|
658
699
|
every command and flag with copy-paste examples, a safe session that tries
|
|
659
700
|
everything on your own machine, exit codes, and error messages
|
|
701
|
+
- **[Web API guide](https://github.com/sameeralam3127/ipmg/blob/main/docs/API.md)** —
|
|
702
|
+
script IPMG Web over HTTP: start scans, fetch results, compare scans,
|
|
703
|
+
and follow live events
|
|
660
704
|
- **[Troubleshooting](https://github.com/sameeralam3127/ipmg/blob/main/docs/TROUBLESHOOTING.md)** —
|
|
661
705
|
install errors, `command not found`, every host timing out, slow scans,
|
|
662
706
|
rejected input files
|
|
@@ -670,11 +714,11 @@ Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
|
|
|
670
714
|
## Contributing
|
|
671
715
|
|
|
672
716
|
Contributions are welcome.
|
|
673
|
-
[CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/CONTRIBUTING.md)
|
|
717
|
+
[CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/.github/CONTRIBUTING.md)
|
|
674
718
|
covers setting up a development environment, running the tests, the commit
|
|
675
719
|
message format that drives automated releases, and how the project website and
|
|
676
720
|
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).
|
|
721
|
+
[Code of Conduct](https://github.com/sameeralam3127/ipmg/blob/main/.github/CODE_OF_CONDUCT.md).
|
|
678
722
|
|
|
679
723
|
---
|
|
680
724
|
|
|
@@ -343,6 +343,21 @@ ipmg diff --fail-on-change # exit 2 when anything changed (CI)
|
|
|
343
343
|
ipmg history --limit 10 # list stored scans
|
|
344
344
|
```
|
|
345
345
|
|
|
346
|
+
To hear about changes without watching the terminal, send them to Slack,
|
|
347
|
+
Microsoft Teams, any JSON webhook, or email. Any `--notify-*` flag turns on
|
|
348
|
+
`--compare`, and with `--interval` every pass that changes something alerts:
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
ipmg --input targets.txt --interval 15 --notify-slack # URL from $IPMG_NOTIFY_SLACK
|
|
352
|
+
ipmg --input targets.txt --notify-email ops@example.com --smtp-host smtp.example.com
|
|
353
|
+
ipmg diff --notify-webhook https://ops.example.com/ipmg --notify-severity critical
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Only changes at or above `--notify-severity` (default `warning`) trigger a
|
|
357
|
+
notification. A notification that fails is reported but never fails the scan.
|
|
358
|
+
Secrets such as webhook URLs and `IPMG_SMTP_PASSWORD` can come from environment
|
|
359
|
+
variables; see [Notifications](docs/COMMANDS.md#notifications) for all of them.
|
|
360
|
+
|
|
346
361
|
What counts as a change:
|
|
347
362
|
|
|
348
363
|
| Change | Severity | Meaning |
|
|
@@ -375,6 +390,11 @@ source**, so file-based and `--discover` runs do not get mixed up. Pass
|
|
|
375
390
|
| `--latency-threshold` | `5` | Minimum latency delta in ms |
|
|
376
391
|
| `--latency-pct` | `25` | Minimum relative latency change |
|
|
377
392
|
| `--fail-on-change` | off | `ipmg diff` exits 2 when changes are found |
|
|
393
|
+
| `--notify-webhook` | off | POST the change report as JSON to a URL |
|
|
394
|
+
| `--notify-slack` | off | Post changes to a Slack incoming webhook |
|
|
395
|
+
| `--notify-teams` | off | Post changes to a Microsoft Teams Workflows webhook |
|
|
396
|
+
| `--notify-email` | off | Email the Markdown change report (with `--smtp-host`, `--smtp-port`, `--smtp-security`, `--smtp-user`, `--smtp-from`) |
|
|
397
|
+
| `--notify-severity` | `warning` | Only notify for changes at least this severe |
|
|
378
398
|
|
|
379
399
|
---
|
|
380
400
|
|
|
@@ -415,6 +435,14 @@ package, nothing is loaded from a CDN. It gives you:
|
|
|
415
435
|
| `--no-browser` | off | Don't open the browser automatically |
|
|
416
436
|
| `--db` | `~/.ipmg/dashboard.db` | History database location |
|
|
417
437
|
|
|
438
|
+
### Scripting IPMG Web
|
|
439
|
+
|
|
440
|
+
Everything the dashboard does goes through a documented, versioned REST API
|
|
441
|
+
under `/api/v1`, so you can start scans, fetch results, and compare scans from
|
|
442
|
+
your own scripts. The [API guide](https://github.com/sameeralam3127/ipmg/blob/main/docs/API.md)
|
|
443
|
+
covers authentication, errors, and worked `curl` examples; the running server
|
|
444
|
+
also serves interactive docs at `http://127.0.0.1:8080/docs`.
|
|
445
|
+
|
|
418
446
|
### On a server with no browser
|
|
419
447
|
|
|
420
448
|
On a Linux server with no display (e.g. accessed over plain SSH), IPMG detects
|
|
@@ -570,12 +598,21 @@ them — so piping IPMG into a file or a log gives you clean text.
|
|
|
570
598
|
| `--stream-refresh` | `0.25` | Seconds between progress-bar redraws while streaming (0.05-5) |
|
|
571
599
|
| `--verbose` | off | Debug logging |
|
|
572
600
|
|
|
601
|
+
**Exit status**
|
|
602
|
+
|
|
603
|
+
| Flag | Default | Description |
|
|
604
|
+
| --- | --- | --- |
|
|
605
|
+
| `--fail-on-down` | off | Exit 3 if any target is not `Active` (reports are still written) |
|
|
606
|
+
| `--min-active` | off | Exit 3 if fewer than this percentage of targets are `Active` |
|
|
607
|
+
|
|
573
608
|
**History and changes** — see [Change detection](#change-detection) for
|
|
574
609
|
`--compare`, `--no-history`, `--db`, `--diff-formats`, `--diff-output`,
|
|
575
|
-
`--latency-threshold`, `--latency-pct`,
|
|
610
|
+
`--latency-threshold`, `--latency-pct`, `--fail-on-change`, and the
|
|
611
|
+
`--notify-*` flags.
|
|
576
612
|
|
|
577
613
|
Exit codes: `0` success, `1` error, `2` changes detected
|
|
578
|
-
(`ipmg diff --fail-on-change`), `
|
|
614
|
+
(`ipmg diff --fail-on-change`), `3` hosts down (`--fail-on-down`,
|
|
615
|
+
`--min-active`), `130` interrupted.
|
|
579
616
|
|
|
580
617
|
---
|
|
581
618
|
|
|
@@ -596,12 +633,15 @@ itself is hardened accordingly:
|
|
|
596
633
|
- Uploads are capped at 5 MB and one scan expands to at most 65,536 hosts,
|
|
597
634
|
so a bad input file cannot exhaust memory
|
|
598
635
|
- All database access uses parameterized SQL
|
|
636
|
+
- The only outbound requests IPMG makes are the notifications you ask for
|
|
637
|
+
with a `--notify-*` flag. Their URLs and the SMTP password can come from
|
|
638
|
+
environment variables, and error messages never repeat them
|
|
599
639
|
|
|
600
640
|
If you bind to a non-local address with `--host`, the token still guards the
|
|
601
641
|
API. It travels over plain HTTP, though, so use an SSH tunnel or a reverse
|
|
602
642
|
proxy with TLS on any network you don't trust.
|
|
603
643
|
|
|
604
|
-
Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
|
|
644
|
+
Found a vulnerability? See [SECURITY.md](.github/SECURITY.md) for how to report it.
|
|
605
645
|
|
|
606
646
|
---
|
|
607
647
|
|
|
@@ -610,6 +650,9 @@ Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
|
|
|
610
650
|
- **[Command reference](https://github.com/sameeralam3127/ipmg/blob/main/docs/COMMANDS.md)** —
|
|
611
651
|
every command and flag with copy-paste examples, a safe session that tries
|
|
612
652
|
everything on your own machine, exit codes, and error messages
|
|
653
|
+
- **[Web API guide](https://github.com/sameeralam3127/ipmg/blob/main/docs/API.md)** —
|
|
654
|
+
script IPMG Web over HTTP: start scans, fetch results, compare scans,
|
|
655
|
+
and follow live events
|
|
613
656
|
- **[Troubleshooting](https://github.com/sameeralam3127/ipmg/blob/main/docs/TROUBLESHOOTING.md)** —
|
|
614
657
|
install errors, `command not found`, every host timing out, slow scans,
|
|
615
658
|
rejected input files
|
|
@@ -623,11 +666,11 @@ Found a vulnerability? See [SECURITY.md](SECURITY.md) for how to report it.
|
|
|
623
666
|
## Contributing
|
|
624
667
|
|
|
625
668
|
Contributions are welcome.
|
|
626
|
-
[CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/CONTRIBUTING.md)
|
|
669
|
+
[CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/.github/CONTRIBUTING.md)
|
|
627
670
|
covers setting up a development environment, running the tests, the commit
|
|
628
671
|
message format that drives automated releases, and how the project website and
|
|
629
672
|
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).
|
|
673
|
+
[Code of Conduct](https://github.com/sameeralam3127/ipmg/blob/main/.github/CODE_OF_CONDUCT.md).
|
|
631
674
|
|
|
632
675
|
---
|
|
633
676
|
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "ipmg"
|
|
16
|
-
version = "2.
|
|
16
|
+
version = "2.3.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,6 +53,7 @@ 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",
|
|
@@ -109,6 +110,9 @@ commit_message = "chore(release): {version}"
|
|
|
109
110
|
build_command = "python -m build"
|
|
110
111
|
major_on_zero = false
|
|
111
112
|
|
|
113
|
+
[tool.semantic_release.changelog.default_templates]
|
|
114
|
+
changelog_file = ".github/CHANGELOG.md"
|
|
115
|
+
|
|
112
116
|
|
|
113
117
|
# ==========================================================
|
|
114
118
|
# Setuptools (src layout)
|
|
@@ -13,8 +13,10 @@ from ipmg.cli.parser import (
|
|
|
13
13
|
build_web_parser,
|
|
14
14
|
)
|
|
15
15
|
from ipmg.core.diff import DiffOptions
|
|
16
|
+
from ipmg.core.health import HostsDownError
|
|
16
17
|
from ipmg.core.security import print_disclaimer_once
|
|
17
18
|
from ipmg.exceptions import IPMGError
|
|
19
|
+
from ipmg.infrastructure.notify import notify_options, send_notifications
|
|
18
20
|
from ipmg.reporting import ui
|
|
19
21
|
from ipmg.reporting.diff_report import export_diff, print_diff
|
|
20
22
|
from ipmg.reporting.summary import print_scan_history
|
|
@@ -25,6 +27,7 @@ from ipmg.utils.helpers import configure_logging
|
|
|
25
27
|
EXIT_OK = 0
|
|
26
28
|
EXIT_ERROR = 1
|
|
27
29
|
EXIT_CHANGES_DETECTED = 2
|
|
30
|
+
EXIT_HOSTS_DOWN = 3
|
|
28
31
|
EXIT_INTERRUPTED = 130
|
|
29
32
|
|
|
30
33
|
log = logging.getLogger(__name__)
|
|
@@ -77,6 +80,7 @@ def _diff_command(argv: List[str]) -> int:
|
|
|
77
80
|
ui.error("Provide at most two scan ids: BASELINE TARGET.")
|
|
78
81
|
return EXIT_ERROR
|
|
79
82
|
|
|
83
|
+
notify = notify_options(args)
|
|
80
84
|
history = HistoryService.open(args.db)
|
|
81
85
|
options = DiffOptions(
|
|
82
86
|
latency_abs_ms=max(args.latency_threshold, 0.0),
|
|
@@ -93,6 +97,7 @@ def _diff_command(argv: List[str]) -> int:
|
|
|
93
97
|
print_diff(diff, limit=args.limit)
|
|
94
98
|
if args.diff_formats:
|
|
95
99
|
export_diff(diff, args.diff_output, args.diff_formats)
|
|
100
|
+
send_notifications(diff, notify)
|
|
96
101
|
|
|
97
102
|
if args.fail_on_change and diff.has_changes:
|
|
98
103
|
return EXIT_CHANGES_DETECTED
|
|
@@ -134,6 +139,12 @@ def run(argv: Optional[List[str]] = None) -> int:
|
|
|
134
139
|
|
|
135
140
|
try:
|
|
136
141
|
return handler(handler_argv)
|
|
142
|
+
except HostsDownError as exc:
|
|
143
|
+
# Not an error: the scan finished and wrote its reports, but failed
|
|
144
|
+
# the check it was asked to make.
|
|
145
|
+
ui.blank()
|
|
146
|
+
ui.warn(str(exc))
|
|
147
|
+
return EXIT_HOSTS_DOWN
|
|
137
148
|
except IPMGError as exc:
|
|
138
149
|
ui.blank()
|
|
139
150
|
ui.error(str(exc))
|
|
@@ -13,6 +13,16 @@ from ipmg.infrastructure.incremental import (
|
|
|
13
13
|
MIN_AUTOSAVE_S,
|
|
14
14
|
REPORT_FORMATS,
|
|
15
15
|
)
|
|
16
|
+
from ipmg.infrastructure.notify import (
|
|
17
|
+
DEFAULT_NOTIFY_SEVERITY,
|
|
18
|
+
ENV_EMAIL,
|
|
19
|
+
ENV_SLACK,
|
|
20
|
+
ENV_SMTP_PASSWORD,
|
|
21
|
+
ENV_TEAMS,
|
|
22
|
+
ENV_WEBHOOK,
|
|
23
|
+
NOTIFY_SEVERITIES,
|
|
24
|
+
SMTP_SECURITY,
|
|
25
|
+
)
|
|
16
26
|
from ipmg.reporting.diff_report import DIFF_FORMATS
|
|
17
27
|
from ipmg.reporting.live import DEFAULT_REFRESH_S, MAX_REFRESH_S, MIN_REFRESH_S
|
|
18
28
|
|
|
@@ -43,6 +53,16 @@ def _port_list(value: str) -> tuple:
|
|
|
43
53
|
raise argparse.ArgumentTypeError(str(exc)) from exc
|
|
44
54
|
|
|
45
55
|
|
|
56
|
+
def _percent(value: str) -> float:
|
|
57
|
+
try:
|
|
58
|
+
percent = float(value)
|
|
59
|
+
except ValueError:
|
|
60
|
+
raise argparse.ArgumentTypeError(f"invalid percentage: {value!r}") from None
|
|
61
|
+
if not 0 <= percent <= 100:
|
|
62
|
+
raise argparse.ArgumentTypeError(f"percentage out of range (0-100): {value}")
|
|
63
|
+
return percent
|
|
64
|
+
|
|
65
|
+
|
|
46
66
|
def _add_database_argument(parser) -> None:
|
|
47
67
|
parser.add_argument(
|
|
48
68
|
"--db",
|
|
@@ -87,6 +107,84 @@ def _add_diff_export_arguments(parser) -> None:
|
|
|
87
107
|
)
|
|
88
108
|
|
|
89
109
|
|
|
110
|
+
def _add_notify_arguments(parser: argparse.ArgumentParser) -> None:
|
|
111
|
+
group = parser.add_argument_group(
|
|
112
|
+
"change notifications",
|
|
113
|
+
"Send the change report when a comparison finds changes. Each URL flag may be "
|
|
114
|
+
"given without a value to read it from its environment variable, which keeps "
|
|
115
|
+
"webhook secrets out of the process list and your crontab.",
|
|
116
|
+
)
|
|
117
|
+
group.add_argument(
|
|
118
|
+
"--notify-webhook",
|
|
119
|
+
nargs="?",
|
|
120
|
+
const="",
|
|
121
|
+
default=None,
|
|
122
|
+
metavar="URL",
|
|
123
|
+
help=f"POST the change report as JSON to URL (or ${ENV_WEBHOOK}).",
|
|
124
|
+
)
|
|
125
|
+
group.add_argument(
|
|
126
|
+
"--notify-slack",
|
|
127
|
+
nargs="?",
|
|
128
|
+
const="",
|
|
129
|
+
default=None,
|
|
130
|
+
metavar="URL",
|
|
131
|
+
help=f"Post the changes to a Slack incoming webhook (or ${ENV_SLACK}).",
|
|
132
|
+
)
|
|
133
|
+
group.add_argument(
|
|
134
|
+
"--notify-teams",
|
|
135
|
+
nargs="?",
|
|
136
|
+
const="",
|
|
137
|
+
default=None,
|
|
138
|
+
metavar="URL",
|
|
139
|
+
help=f"Post the changes to a Microsoft Teams Workflows webhook (or ${ENV_TEAMS}).",
|
|
140
|
+
)
|
|
141
|
+
group.add_argument(
|
|
142
|
+
"--notify-email",
|
|
143
|
+
nargs="*",
|
|
144
|
+
default=None,
|
|
145
|
+
metavar="ADDRESS",
|
|
146
|
+
help=(
|
|
147
|
+
"Email the Markdown change report to these addresses "
|
|
148
|
+
f"(or the comma-separated ${ENV_EMAIL}). Needs --smtp-host."
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
group.add_argument(
|
|
152
|
+
"--notify-severity",
|
|
153
|
+
choices=NOTIFY_SEVERITIES,
|
|
154
|
+
default=DEFAULT_NOTIFY_SEVERITY,
|
|
155
|
+
help=(
|
|
156
|
+
"Only notify when a change is at least this severe "
|
|
157
|
+
f"(default: {DEFAULT_NOTIFY_SEVERITY})."
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
group.add_argument(
|
|
161
|
+
"--smtp-host",
|
|
162
|
+
metavar="HOST",
|
|
163
|
+
help="Mail server for --notify-email (or $IPMG_SMTP_HOST).",
|
|
164
|
+
)
|
|
165
|
+
group.add_argument(
|
|
166
|
+
"--smtp-port",
|
|
167
|
+
type=int,
|
|
168
|
+
metavar="PORT",
|
|
169
|
+
help="Mail server port (or $IPMG_SMTP_PORT; default: 587, 465 with ssl, 25 with none).",
|
|
170
|
+
)
|
|
171
|
+
group.add_argument(
|
|
172
|
+
"--smtp-security",
|
|
173
|
+
choices=SMTP_SECURITY,
|
|
174
|
+
help="How to encrypt the mail connection (or $IPMG_SMTP_SECURITY; default: starttls).",
|
|
175
|
+
)
|
|
176
|
+
group.add_argument(
|
|
177
|
+
"--smtp-user",
|
|
178
|
+
metavar="USER",
|
|
179
|
+
help=f"Mail server login (or $IPMG_SMTP_USER). The password is read from ${ENV_SMTP_PASSWORD}.",
|
|
180
|
+
)
|
|
181
|
+
group.add_argument(
|
|
182
|
+
"--smtp-from",
|
|
183
|
+
metavar="ADDRESS",
|
|
184
|
+
help="Sender address (or $IPMG_SMTP_FROM; default: the login, else ipmg@<this host>).",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
90
188
|
def build_parser() -> argparse.ArgumentParser:
|
|
91
189
|
parser = ScanParser(
|
|
92
190
|
prog="ipmg",
|
|
@@ -178,6 +276,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
178
276
|
help="Start IPMG Web, the local browser UI (same as 'ipmg web').",
|
|
179
277
|
)
|
|
180
278
|
|
|
279
|
+
checks = parser.add_argument_group("exit status (for cron and monitoring)")
|
|
280
|
+
checks.add_argument(
|
|
281
|
+
"--fail-on-down",
|
|
282
|
+
action="store_true",
|
|
283
|
+
help="Exit with status 3 if any target is not Active. Reports are still written.",
|
|
284
|
+
)
|
|
285
|
+
checks.add_argument(
|
|
286
|
+
"--min-active",
|
|
287
|
+
type=_percent,
|
|
288
|
+
default=None,
|
|
289
|
+
metavar="PERCENT",
|
|
290
|
+
help="Exit with status 3 if fewer than PERCENT of the targets are Active (0-100).",
|
|
291
|
+
)
|
|
292
|
+
|
|
181
293
|
ports_group = parser.add_argument_group("TCP service discovery")
|
|
182
294
|
ports_group.add_argument(
|
|
183
295
|
"--scan-ports",
|
|
@@ -264,7 +376,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
264
376
|
history.add_argument(
|
|
265
377
|
"--compare",
|
|
266
378
|
action="store_true",
|
|
267
|
-
help=
|
|
379
|
+
help=(
|
|
380
|
+
"Compare this scan against the previous one and print a change report "
|
|
381
|
+
"(implied by any --notify-* flag)."
|
|
382
|
+
),
|
|
268
383
|
)
|
|
269
384
|
history.add_argument(
|
|
270
385
|
"--compare-any-source",
|
|
@@ -274,6 +389,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
274
389
|
_add_database_argument(history)
|
|
275
390
|
_add_diff_export_arguments(history)
|
|
276
391
|
_add_diff_threshold_arguments(parser)
|
|
392
|
+
_add_notify_arguments(parser)
|
|
277
393
|
parser.set_defaults(history=True)
|
|
278
394
|
return parser
|
|
279
395
|
|
|
@@ -363,5 +479,6 @@ def build_diff_parser() -> argparse.ArgumentParser:
|
|
|
363
479
|
_add_database_argument(parser)
|
|
364
480
|
_add_diff_export_arguments(parser)
|
|
365
481
|
_add_diff_threshold_arguments(parser)
|
|
482
|
+
_add_notify_arguments(parser)
|
|
366
483
|
parser.add_argument("--verbose", action="store_true")
|
|
367
484
|
return parser
|
|
@@ -48,6 +48,12 @@ _SEVERITY_BY_TYPE: Dict[ChangeType, Severity] = {
|
|
|
48
48
|
|
|
49
49
|
_SEVERITY_RANK = {Severity.CRITICAL: 0, Severity.WARNING: 1, Severity.INFO: 2}
|
|
50
50
|
|
|
51
|
+
|
|
52
|
+
def meets_severity(severity: Severity, minimum: Severity) -> bool:
|
|
53
|
+
"""Whether ``severity`` is at least as serious as ``minimum``."""
|
|
54
|
+
return _SEVERITY_RANK[severity] <= _SEVERITY_RANK[minimum]
|
|
55
|
+
|
|
56
|
+
|
|
51
57
|
_TYPE_RANK = {change_type: index for index, change_type in enumerate(ChangeType)}
|
|
52
58
|
|
|
53
59
|
CHANGE_LABELS: Dict[ChangeType, str] = {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Pass/fail checks on a finished scan, for cron jobs and monitoring probes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional, Sequence
|
|
7
|
+
|
|
8
|
+
from ipmg.core.diff import ip_sort_key
|
|
9
|
+
from ipmg.core.engine import HostResult
|
|
10
|
+
from ipmg.exceptions import IPMGError
|
|
11
|
+
|
|
12
|
+
ACTIVE_STATUS = "Active"
|
|
13
|
+
|
|
14
|
+
#: How many down hosts a failure message names before it summarises the rest.
|
|
15
|
+
MAX_NAMED_HOSTS = 5
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HostsDownError(IPMGError):
|
|
19
|
+
"""Raised after a finished scan fails --fail-on-down or --min-active."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class HealthPolicy:
|
|
24
|
+
"""When a scan counts as failed. The default policy never fails."""
|
|
25
|
+
|
|
26
|
+
#: Fail if any target is not ``Active``.
|
|
27
|
+
fail_on_down: bool = False
|
|
28
|
+
#: Fail if fewer than this percentage of targets are ``Active``.
|
|
29
|
+
min_active_pct: Optional[float] = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def enabled(self) -> bool:
|
|
33
|
+
return self.fail_on_down or self.min_active_pct is not None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _name_hosts(ips: Sequence[str]) -> str:
|
|
37
|
+
# Results arrive in completion order; the message should read like the target list.
|
|
38
|
+
ips = sorted(ips, key=ip_sort_key)
|
|
39
|
+
named = ", ".join(ips[:MAX_NAMED_HOSTS])
|
|
40
|
+
hidden = len(ips) - MAX_NAMED_HOSTS
|
|
41
|
+
return f"{named} and {hidden} more" if hidden > 0 else named
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _format_pct(value: float) -> str:
|
|
45
|
+
return f"{value:.1f}".rstrip("0").rstrip(".") + "%"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_health(results: Sequence[HostResult], policy: HealthPolicy) -> Optional[str]:
|
|
49
|
+
"""Why ``results`` fail ``policy``, or None when they pass."""
|
|
50
|
+
if not policy.enabled:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
total = len(results)
|
|
54
|
+
down = [result.ip for result in results if result.status != ACTIVE_STATUS]
|
|
55
|
+
active_pct = (total - len(down)) / total * 100 if total else 0.0
|
|
56
|
+
|
|
57
|
+
if policy.min_active_pct is not None and active_pct < policy.min_active_pct:
|
|
58
|
+
return (
|
|
59
|
+
f"Only {_format_pct(active_pct)} of {total} hosts are active, "
|
|
60
|
+
f"below --min-active {_format_pct(policy.min_active_pct)}."
|
|
61
|
+
)
|
|
62
|
+
if policy.fail_on_down and down:
|
|
63
|
+
return f"{len(down)} of {total} hosts are not active: {_name_hosts(down)}."
|
|
64
|
+
return None
|