From 1a11244fff61e52cfcad623abaacaa91d428ac16 Mon Sep 17 00:00:00 2001 From: p2913020 Date: Fri, 22 May 2026 00:19:30 -0400 Subject: [PATCH] Created IAC reverse generator --- .kiro/specs/iac-reverse-engineering/tasks.md | 102 +-- pyproject.toml | 42 + src/iac_reverse.egg-info/PKG-INFO | 23 + src/iac_reverse.egg-info/SOURCES.txt | 17 + src/iac_reverse.egg-info/dependency_links.txt | 1 + src/iac_reverse.egg-info/entry_points.txt | 2 + src/iac_reverse.egg-info/requires.txt | 13 + src/iac_reverse.egg-info/top_level.txt | 1 + src/iac_reverse/__init__.py | 58 ++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 1147 bytes .../__pycache__/models.cpython-313.pyc | Bin 0 -> 15440 bytes .../__pycache__/plugin_base.cpython-313.pyc | Bin 0 -> 4111 bytes src/iac_reverse/auth/__init__.py | 21 + .../auth/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 565 bytes .../authentik_auth.cpython-313.pyc | Bin 0 -> 7647 bytes .../authentik_discovery.cpython-313.pyc | Bin 0 -> 13286 bytes src/iac_reverse/auth/authentik_auth.py | 204 +++++ src/iac_reverse/auth/authentik_discovery.py | 384 ++++++++ src/iac_reverse/cli/__init__.py | 6 + .../cli/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 409 bytes .../cli/__pycache__/cli.cpython-313.pyc | Bin 0 -> 21345 bytes .../profile_loader.cpython-313.pyc | Bin 0 -> 7222 bytes src/iac_reverse/cli/cli.py | 444 ++++++++++ src/iac_reverse/cli/profile_loader.py | 184 ++++ src/iac_reverse/generator/__init__.py | 15 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 664 bytes .../code_generator.cpython-313.pyc | Bin 0 -> 10424 bytes .../provider_block.cpython-313.pyc | Bin 0 -> 8667 bytes .../resource_merger.cpython-313.pyc | Bin 0 -> 2767 bytes .../__pycache__/sanitize.cpython-313.pyc | Bin 0 -> 1381 bytes .../variable_extractor.cpython-313.pyc | Bin 0 -> 7826 bytes src/iac_reverse/generator/code_generator.py | 304 +++++++ src/iac_reverse/generator/provider_block.py | 197 +++++ src/iac_reverse/generator/resource_merger.py | 59 ++ src/iac_reverse/generator/sanitize.py | 41 + .../generator/variable_extractor.py | 203 +++++ src/iac_reverse/incremental/__init__.py | 7 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 501 bytes .../change_detector.cpython-313.pyc | Bin 0 -> 5755 bytes .../incremental_updater.cpython-313.pyc | Bin 0 -> 14663 bytes .../snapshot_store.cpython-313.pyc | Bin 0 -> 8210 bytes .../incremental/change_detector.py | 144 +++ .../incremental/incremental_updater.py | 339 +++++++ src/iac_reverse/incremental/snapshot_store.py | 177 ++++ src/iac_reverse/models.py | 425 +++++++++ src/iac_reverse/plugin_base.py | 103 +++ src/iac_reverse/resolver/__init__.py | 5 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 333 bytes .../__pycache__/resolver.cpython-313.pyc | Bin 0 -> 14864 bytes src/iac_reverse/resolver/resolver.py | 443 ++++++++++ src/iac_reverse/scanner/__init__.py | 45 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 1237 bytes .../bare_metal_plugin.cpython-313.pyc | Bin 0 -> 20264 bytes .../docker_swarm_plugin.cpython-313.pyc | Bin 0 -> 15671 bytes .../harvester_plugin.cpython-313.pyc | Bin 0 -> 15971 bytes .../kubernetes_plugin.cpython-313.pyc | Bin 0 -> 17200 bytes .../multi_provider_scanner.cpython-313.pyc | Bin 0 -> 6387 bytes .../__pycache__/scanner.cpython-313.pyc | Bin 0 -> 11608 bytes .../synology_plugin.cpython-313.pyc | Bin 0 -> 16121 bytes .../windows_plugin.cpython-313.pyc | Bin 0 -> 30136 bytes src/iac_reverse/scanner/bare_metal_plugin.py | 497 +++++++++++ .../scanner/docker_swarm_plugin.py | 433 +++++++++ src/iac_reverse/scanner/harvester_plugin.py | 458 ++++++++++ src/iac_reverse/scanner/kubernetes_plugin.py | 454 ++++++++++ .../scanner/multi_provider_scanner.py | 140 +++ src/iac_reverse/scanner/scanner.py | 287 ++++++ src/iac_reverse/scanner/synology_plugin.py | 482 ++++++++++ src/iac_reverse/scanner/windows_plugin.py | 825 ++++++++++++++++++ src/iac_reverse/state_builder/__init__.py | 5 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 338 bytes .../__pycache__/state_builder.cpython-313.pyc | Bin 0 -> 11807 bytes .../state_builder/state_builder.py | 332 +++++++ src/iac_reverse/validator/__init__.py | 5 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 314 bytes .../__pycache__/validator.cpython-313.pyc | Bin 0 -> 21436 bytes src/iac_reverse/validator/validator.py | 653 ++++++++++++++ tests/__init__.py | 1 + tests/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 191 bytes tests/integration/__init__.py | 1 + tests/property/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 210 bytes ...enerator_prop.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 53842 bytes ...resolver_prop.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 48993 bytes ...t_report_prop.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 23116 bytes ...tal_scan_prop.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 57091 bytes ...provider_prop.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 49514 bytes ...leteness_prop.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 47299 bytes ...lidation_prop.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 25626 bytes ...behavior_prop.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 39933 bytes ..._builder_prop.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 44175 bytes tests/property/test_code_generator_prop.py | 719 +++++++++++++++ .../property/test_dependency_resolver_prop.py | 565 ++++++++++++ tests/property/test_drift_report_prop.py | 308 +++++++ tests/property/test_incremental_scan_prop.py | 790 +++++++++++++++++ tests/property/test_multi_provider_prop.py | 803 +++++++++++++++++ ...st_resource_inventory_completeness_prop.py | 222 +++++ .../test_scan_profile_validation_prop.py | 257 ++++++ tests/property/test_scanner_behavior_prop.py | 608 +++++++++++++ tests/property/test_state_builder_prop.py | 567 ++++++++++++ tests/unit/__init__.py | 1 + .../unit/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 196 bytes ...est_authentik.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 59251 bytes ..._metal_plugin.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 71315 bytes ...ange_detector.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 53673 bytes .../test_cli.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 55364 bytes ...ode_generator.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 46502 bytes ..._swarm_plugin.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 55588 bytes ...vester_plugin.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 58757 bytes ...ental_updater.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 33370 bytes ...rnetes_plugin.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 60857 bytes .../test_models.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 74314 bytes ...vider_scanner.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 50912 bytes ...t_plugin_base.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 9713 bytes ...rofile_loader.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 34975 bytes ...rovider_block.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 52036 bytes ...test_resolver.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 43027 bytes ...solver_cycles.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 41685 bytes ...er_unresolved.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 43205 bytes ...source_merger.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 33045 bytes ...test_sanitize.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 19602 bytes ...le_validation.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 26274 bytes .../test_scanner.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 49108 bytes ...ner_filtering.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 25818 bytes ...napshot_store.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 37491 bytes ...state_builder.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 50350 bytes ...lder_unmapped.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 40245 bytes ...nology_plugin.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 68821 bytes ...est_validator.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 67801 bytes ...r_autocorrect.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 44424 bytes ...ble_extractor.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 34992 bytes ...indows_plugin.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 58697 bytes tests/unit/test_authentik.py | 495 +++++++++++ tests/unit/test_bare_metal_plugin.py | 664 ++++++++++++++ tests/unit/test_change_detector.py | 335 +++++++ tests/unit/test_cli.py | 437 ++++++++++ tests/unit/test_code_generator.py | 639 ++++++++++++++ tests/unit/test_docker_swarm_plugin.py | 444 ++++++++++ tests/unit/test_harvester_plugin.py | 569 ++++++++++++ tests/unit/test_incremental_updater.py | 513 +++++++++++ tests/unit/test_kubernetes_plugin.py | 508 +++++++++++ tests/unit/test_models.py | 403 +++++++++ tests/unit/test_multi_provider_scanner.py | 505 +++++++++++ tests/unit/test_plugin_base.py | 74 ++ tests/unit/test_profile_loader.py | 324 +++++++ tests/unit/test_provider_block.py | 414 +++++++++ tests/unit/test_resolver.py | 481 ++++++++++ tests/unit/test_resolver_cycles.py | 537 ++++++++++++ tests/unit/test_resolver_unresolved.py | 445 ++++++++++ tests/unit/test_resource_merger.py | 343 ++++++++ tests/unit/test_sanitize.py | 115 +++ tests/unit/test_scan_profile_validation.py | 193 ++++ tests/unit/test_scanner.py | 480 ++++++++++ tests/unit/test_scanner_filtering.py | 234 +++++ tests/unit/test_snapshot_store.py | 245 ++++++ tests/unit/test_state_builder.py | 681 +++++++++++++++ tests/unit/test_state_builder_unmapped.py | 468 ++++++++++ tests/unit/test_synology_plugin.py | 499 +++++++++++ tests/unit/test_validator.py | 689 +++++++++++++++ tests/unit/test_validator_autocorrect.py | 766 ++++++++++++++++ tests/unit/test_variable_extractor.py | 405 +++++++++ tests/unit/test_windows_plugin.py | 529 +++++++++++ 161 files changed, 26806 insertions(+), 51 deletions(-) create mode 100644 pyproject.toml create mode 100644 src/iac_reverse.egg-info/PKG-INFO create mode 100644 src/iac_reverse.egg-info/SOURCES.txt create mode 100644 src/iac_reverse.egg-info/dependency_links.txt create mode 100644 src/iac_reverse.egg-info/entry_points.txt create mode 100644 src/iac_reverse.egg-info/requires.txt create mode 100644 src/iac_reverse.egg-info/top_level.txt create mode 100644 src/iac_reverse/__init__.py create mode 100644 src/iac_reverse/__pycache__/__init__.cpython-313.pyc create mode 100644 src/iac_reverse/__pycache__/models.cpython-313.pyc create mode 100644 src/iac_reverse/__pycache__/plugin_base.cpython-313.pyc create mode 100644 src/iac_reverse/auth/__init__.py create mode 100644 src/iac_reverse/auth/__pycache__/__init__.cpython-313.pyc create mode 100644 src/iac_reverse/auth/__pycache__/authentik_auth.cpython-313.pyc create mode 100644 src/iac_reverse/auth/__pycache__/authentik_discovery.cpython-313.pyc create mode 100644 src/iac_reverse/auth/authentik_auth.py create mode 100644 src/iac_reverse/auth/authentik_discovery.py create mode 100644 src/iac_reverse/cli/__init__.py create mode 100644 src/iac_reverse/cli/__pycache__/__init__.cpython-313.pyc create mode 100644 src/iac_reverse/cli/__pycache__/cli.cpython-313.pyc create mode 100644 src/iac_reverse/cli/__pycache__/profile_loader.cpython-313.pyc create mode 100644 src/iac_reverse/cli/cli.py create mode 100644 src/iac_reverse/cli/profile_loader.py create mode 100644 src/iac_reverse/generator/__init__.py create mode 100644 src/iac_reverse/generator/__pycache__/__init__.cpython-313.pyc create mode 100644 src/iac_reverse/generator/__pycache__/code_generator.cpython-313.pyc create mode 100644 src/iac_reverse/generator/__pycache__/provider_block.cpython-313.pyc create mode 100644 src/iac_reverse/generator/__pycache__/resource_merger.cpython-313.pyc create mode 100644 src/iac_reverse/generator/__pycache__/sanitize.cpython-313.pyc create mode 100644 src/iac_reverse/generator/__pycache__/variable_extractor.cpython-313.pyc create mode 100644 src/iac_reverse/generator/code_generator.py create mode 100644 src/iac_reverse/generator/provider_block.py create mode 100644 src/iac_reverse/generator/resource_merger.py create mode 100644 src/iac_reverse/generator/sanitize.py create mode 100644 src/iac_reverse/generator/variable_extractor.py create mode 100644 src/iac_reverse/incremental/__init__.py create mode 100644 src/iac_reverse/incremental/__pycache__/__init__.cpython-313.pyc create mode 100644 src/iac_reverse/incremental/__pycache__/change_detector.cpython-313.pyc create mode 100644 src/iac_reverse/incremental/__pycache__/incremental_updater.cpython-313.pyc create mode 100644 src/iac_reverse/incremental/__pycache__/snapshot_store.cpython-313.pyc create mode 100644 src/iac_reverse/incremental/change_detector.py create mode 100644 src/iac_reverse/incremental/incremental_updater.py create mode 100644 src/iac_reverse/incremental/snapshot_store.py create mode 100644 src/iac_reverse/models.py create mode 100644 src/iac_reverse/plugin_base.py create mode 100644 src/iac_reverse/resolver/__init__.py create mode 100644 src/iac_reverse/resolver/__pycache__/__init__.cpython-313.pyc create mode 100644 src/iac_reverse/resolver/__pycache__/resolver.cpython-313.pyc create mode 100644 src/iac_reverse/resolver/resolver.py create mode 100644 src/iac_reverse/scanner/__init__.py create mode 100644 src/iac_reverse/scanner/__pycache__/__init__.cpython-313.pyc create mode 100644 src/iac_reverse/scanner/__pycache__/bare_metal_plugin.cpython-313.pyc create mode 100644 src/iac_reverse/scanner/__pycache__/docker_swarm_plugin.cpython-313.pyc create mode 100644 src/iac_reverse/scanner/__pycache__/harvester_plugin.cpython-313.pyc create mode 100644 src/iac_reverse/scanner/__pycache__/kubernetes_plugin.cpython-313.pyc create mode 100644 src/iac_reverse/scanner/__pycache__/multi_provider_scanner.cpython-313.pyc create mode 100644 src/iac_reverse/scanner/__pycache__/scanner.cpython-313.pyc create mode 100644 src/iac_reverse/scanner/__pycache__/synology_plugin.cpython-313.pyc create mode 100644 src/iac_reverse/scanner/__pycache__/windows_plugin.cpython-313.pyc create mode 100644 src/iac_reverse/scanner/bare_metal_plugin.py create mode 100644 src/iac_reverse/scanner/docker_swarm_plugin.py create mode 100644 src/iac_reverse/scanner/harvester_plugin.py create mode 100644 src/iac_reverse/scanner/kubernetes_plugin.py create mode 100644 src/iac_reverse/scanner/multi_provider_scanner.py create mode 100644 src/iac_reverse/scanner/scanner.py create mode 100644 src/iac_reverse/scanner/synology_plugin.py create mode 100644 src/iac_reverse/scanner/windows_plugin.py create mode 100644 src/iac_reverse/state_builder/__init__.py create mode 100644 src/iac_reverse/state_builder/__pycache__/__init__.cpython-313.pyc create mode 100644 src/iac_reverse/state_builder/__pycache__/state_builder.cpython-313.pyc create mode 100644 src/iac_reverse/state_builder/state_builder.py create mode 100644 src/iac_reverse/validator/__init__.py create mode 100644 src/iac_reverse/validator/__pycache__/__init__.cpython-313.pyc create mode 100644 src/iac_reverse/validator/__pycache__/validator.cpython-313.pyc create mode 100644 src/iac_reverse/validator/validator.py create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-313.pyc create mode 100644 tests/integration/__init__.py create mode 100644 tests/property/__init__.py create mode 100644 tests/property/__pycache__/__init__.cpython-313.pyc create mode 100644 tests/property/__pycache__/test_code_generator_prop.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/property/__pycache__/test_dependency_resolver_prop.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/property/__pycache__/test_drift_report_prop.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/property/__pycache__/test_incremental_scan_prop.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/property/__pycache__/test_multi_provider_prop.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/property/__pycache__/test_resource_inventory_completeness_prop.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/property/__pycache__/test_scan_profile_validation_prop.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/property/__pycache__/test_scanner_behavior_prop.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/property/__pycache__/test_state_builder_prop.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/property/test_code_generator_prop.py create mode 100644 tests/property/test_dependency_resolver_prop.py create mode 100644 tests/property/test_drift_report_prop.py create mode 100644 tests/property/test_incremental_scan_prop.py create mode 100644 tests/property/test_multi_provider_prop.py create mode 100644 tests/property/test_resource_inventory_completeness_prop.py create mode 100644 tests/property/test_scan_profile_validation_prop.py create mode 100644 tests/property/test_scanner_behavior_prop.py create mode 100644 tests/property/test_state_builder_prop.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/__pycache__/__init__.cpython-313.pyc create mode 100644 tests/unit/__pycache__/test_authentik.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_bare_metal_plugin.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_change_detector.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_code_generator.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_docker_swarm_plugin.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_harvester_plugin.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_incremental_updater.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_kubernetes_plugin.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_models.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_multi_provider_scanner.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_plugin_base.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_profile_loader.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_provider_block.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_resolver.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_resolver_cycles.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_resolver_unresolved.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_resource_merger.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_sanitize.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_scan_profile_validation.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_scanner.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_scanner_filtering.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_snapshot_store.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_state_builder.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_state_builder_unmapped.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_synology_plugin.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_validator.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_validator_autocorrect.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_variable_extractor.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/__pycache__/test_windows_plugin.cpython-313-pytest-9.0.3.pyc create mode 100644 tests/unit/test_authentik.py create mode 100644 tests/unit/test_bare_metal_plugin.py create mode 100644 tests/unit/test_change_detector.py create mode 100644 tests/unit/test_cli.py create mode 100644 tests/unit/test_code_generator.py create mode 100644 tests/unit/test_docker_swarm_plugin.py create mode 100644 tests/unit/test_harvester_plugin.py create mode 100644 tests/unit/test_incremental_updater.py create mode 100644 tests/unit/test_kubernetes_plugin.py create mode 100644 tests/unit/test_models.py create mode 100644 tests/unit/test_multi_provider_scanner.py create mode 100644 tests/unit/test_plugin_base.py create mode 100644 tests/unit/test_profile_loader.py create mode 100644 tests/unit/test_provider_block.py create mode 100644 tests/unit/test_resolver.py create mode 100644 tests/unit/test_resolver_cycles.py create mode 100644 tests/unit/test_resolver_unresolved.py create mode 100644 tests/unit/test_resource_merger.py create mode 100644 tests/unit/test_sanitize.py create mode 100644 tests/unit/test_scan_profile_validation.py create mode 100644 tests/unit/test_scanner.py create mode 100644 tests/unit/test_scanner_filtering.py create mode 100644 tests/unit/test_snapshot_store.py create mode 100644 tests/unit/test_state_builder.py create mode 100644 tests/unit/test_state_builder_unmapped.py create mode 100644 tests/unit/test_synology_plugin.py create mode 100644 tests/unit/test_validator.py create mode 100644 tests/unit/test_validator_autocorrect.py create mode 100644 tests/unit/test_variable_extractor.py create mode 100644 tests/unit/test_windows_plugin.py diff --git a/.kiro/specs/iac-reverse-engineering/tasks.md b/.kiro/specs/iac-reverse-engineering/tasks.md index 5382e49..94f9dcc 100644 --- a/.kiro/specs/iac-reverse-engineering/tasks.md +++ b/.kiro/specs/iac-reverse-engineering/tasks.md @@ -6,15 +6,15 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu ## Tasks -- [ ] 1. Set up project structure and core data models - - [ ] 1.1 Create project directory structure, pyproject.toml, and install dependencies +- [x] 1. Set up project structure and core data models + - [x] 1.1 Create project directory structure, pyproject.toml, and install dependencies - Create `src/iac_reverse/` package with `__init__.py` - Create subdirectories: `scanner/`, `resolver/`, `generator/`, `state_builder/`, `validator/`, `incremental/`, `auth/`, `cli/` - Set up `pyproject.toml` with dependencies: kubernetes, docker, pywinrm, hypothesis, pytest, click, jinja2, networkx, pyyaml, python-synology - Create `tests/` directory with `unit/`, `property/`, `integration/` subdirectories - _Requirements: 1.1, 5.1, 5.2_ - - [ ] 1.2 Define core enums, data classes, and interfaces + - [x] 1.2 Define core enums, data classes, and interfaces - Implement `ProviderType` enum (docker_swarm, kubernetes, synology, harvester, bare_metal, windows) - Implement `PlatformCategory` enum (container_orchestration, storage_appliance, hci, bare_metal, windows) and `PROVIDER_PLATFORM_MAP` - Implement `CpuArchitecture` enum (amd64, arm, aarch64) @@ -27,19 +27,19 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu - Define `ProviderPlugin` abstract base class with all abstract methods - _Requirements: 1.1, 1.2, 2.1, 3.1, 4.1, 5.1, 5.2, 8.1_ - - [ ] 1.3 Implement ScanProfile validation logic + - [x] 1.3 Implement ScanProfile validation logic - Validate mandatory fields: provider type and non-empty credentials - Validate optional fields: resource_type_filters max 200 entries, endpoints list - Validate resource types against provider's supported types - Return all validation errors in a single response - _Requirements: 6.1, 6.6, 6.7_ - - [ ]* 1.4 Write property test for scan profile validation (Property 20) + - [x] 1.4 Write property test for scan profile validation (Property 20) - **Property 20: Scan profile validation completeness** - **Validates: Requirements 6.1, 6.6, 6.7** -- [ ] 2. Implement Scanner core and provider plugin system - - [ ] 2.1 Implement Scanner orchestrator with progress reporting and error handling +- [x] 2. Implement Scanner core and provider plugin system + - [x] 2.1 Implement Scanner orchestrator with progress reporting and error handling - Create `Scanner` class that accepts a `ScanProfile` and orchestrates discovery - Implement connection timeout (30 seconds) and authentication error handling with descriptive messages - Implement progress callback invocation per resource type completion @@ -48,44 +48,44 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu - Implement warning logging for unsupported resource types while continuing scan - _Requirements: 1.1, 1.3, 1.4, 1.5, 1.6, 1.7_ - - [ ]* 2.2 Write property tests for Scanner behavior (Properties 2, 3, 4, 5) + - [x] 2.2 Write property tests for Scanner behavior (Properties 2, 3, 4, 5) - **Property 2: Authentication error descriptiveness** - **Property 3: Graceful degradation on unsupported resource types** - **Property 4: Progress reporting frequency** - **Property 5: Partial inventory preservation on failure** - **Validates: Requirements 1.3, 1.4, 1.5, 1.7** - - [ ] 2.3 Implement Docker Swarm provider plugin + - [x] 2.3 Implement Docker Swarm provider plugin - Implement `DockerSwarmPlugin` using docker-sdk-python - Discover services, networks, volumes, configs, secrets (metadata only) - Detect architecture from node info - _Requirements: 1.1, 1.2, 5.2_ - - [ ] 2.4 Implement Kubernetes provider plugin + - [x] 2.4 Implement Kubernetes provider plugin - Implement `KubernetesPlugin` using kubernetes-client - Discover deployments, services, ingresses, config maps, persistent volumes, namespaces - Detect architecture from node labels - _Requirements: 1.1, 1.2, 5.2_ - - [ ] 2.5 Implement Synology provider plugin + - [x] 2.5 Implement Synology provider plugin - Implement `SynologyPlugin` using Synology DSM API - Discover shared folders, volumes, storage pools, replication tasks, users - Detect architecture from system info (ARM vs AMD64) - _Requirements: 1.1, 1.2, 5.2_ - - [ ] 2.6 Implement Harvester provider plugin + - [x] 2.6 Implement Harvester provider plugin - Implement `HarvesterPlugin` using Harvester/K8s-based API - Discover VMs, volumes, images, networks (HCI combined resources) - Detect architecture from node info - _Requirements: 1.1, 1.2, 5.2_ - - [ ] 2.7 Implement Bare Metal provider plugin + - [x] 2.7 Implement Bare Metal provider plugin - Implement `BareMetalPlugin` using IPMI/Redfish API - Discover hardware inventory, BMC configs, network interfaces, RAID configurations - Detect architecture from system hardware info - _Requirements: 1.1, 1.2, 5.2_ - - [ ] 2.8 Implement Windows provider plugin + - [x] 2.8 Implement Windows provider plugin - Implement `WindowsDiscoveryPlugin` using pywinrm library - Authenticate via WinRM using NTLM or Kerberos (configurable transport, port, SSL) - Discover Windows services, scheduled tasks, IIS sites, IIS app pools, network adapters, firewall rules, installed software, Windows features, Hyper-V VMs, Hyper-V switches, DNS records, local users, local groups @@ -94,21 +94,21 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu - Handle WinRM-specific errors: WinRM not enabled, WMI query failure, insufficient privileges - _Requirements: 1.1, 1.2, 5.2_ - - [ ] 2.9 Implement Authentik integration (SSO + discovery plugin) + - [x] 2.9 Implement Authentik integration (SSO + discovery plugin) - Implement `AuthentikAuthProvider` for OAuth2/OIDC SSO flow (authenticate, refresh, validate) - Implement `AuthentikDiscoveryPlugin` conforming to `ProviderPlugin` - Discover flows, stages, providers, applications, outposts, property mappings, certificates, groups, sources - _Requirements: 1.1, 1.2, 5.2_ - - [ ]* 2.10 Write property test for resource inventory completeness (Property 1) + - [x] 2.10 Write property test for resource inventory completeness (Property 1) - **Property 1: Resource inventory completeness** - **Validates: Requirements 1.2** -- [ ] 3. Checkpoint - Ensure all tests pass +- [x] 3. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. -- [ ] 4. Implement Dependency Resolver - - [ ] 4.1 Implement dependency resolution and graph building +- [x] 4. Implement Dependency Resolver + - [x] 4.1 Implement dependency resolution and graph building - Create `DependencyResolver` class - Analyze resource `raw_references` to identify parent-child, reference, and dependency relationships - Build dependency graph using networkx @@ -116,27 +116,27 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu - Represent relationships as explicit Terraform references (not hardcoded IDs) - _Requirements: 2.1, 2.2, 2.4_ - - [ ] 4.2 Implement cycle detection and resolution suggestions + - [x] 4.2 Implement cycle detection and resolution suggestions - Detect circular dependencies in the graph - Report cycles listing all involved resources - Suggest resolution strategies (which relationship to break, data source lookup alternatives) - _Requirements: 2.3_ - - [ ] 4.3 Implement unresolved reference handling + - [x] 4.3 Implement unresolved reference handling - Identify references to IDs not in the current inventory - Log warnings for unresolved references - Represent unresolved references as data source lookups or variables in output - _Requirements: 2.5_ - - [ ]* 4.4 Write property tests for Dependency Resolver (Properties 6, 7, 8, 9) + - [x] 4.4 Write property tests for Dependency Resolver (Properties 6, 7, 8, 9) - **Property 6: Dependency relationship identification** - **Property 7: Cycle detection correctness** - **Property 8: Topological order validity** - **Property 9: Unresolved references become data sources or variables** - **Validates: Requirements 2.1, 2.3, 2.4, 2.5** -- [ ] 5. Implement Code Generator - - [ ] 5.1 Implement HCL code generation with Jinja2 templates +- [x] 5. Implement Code Generator + - [x] 5.1 Implement HCL code generation with Jinja2 templates - Create `CodeGenerator` class - Create Jinja2 templates for Terraform resource blocks per provider/resource type - Generate syntactically valid HCL files from dependency graph @@ -146,31 +146,31 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu - Generate architecture-specific tags/labels on resources - _Requirements: 3.1, 3.2, 3.5, 3.6_ - - [ ] 5.2 Implement identifier sanitization + - [x] 5.2 Implement identifier sanitization - Create `sanitize_identifier()` function - Convert resource names to valid Terraform identifiers: `^[a-zA-Z_][a-zA-Z0-9_]*$` - Handle special characters, unicode, leading digits, spaces by replacing with underscores - Ensure non-empty output for any input - _Requirements: 3.4_ - - [ ] 5.3 Implement variable extraction logic + - [x] 5.3 Implement variable extraction logic - Identify attribute values appearing in 2+ resources - Extract shared values into `variables.tf` with defaults set to most common value - Generate variable declarations with type expressions and descriptions - _Requirements: 3.3_ - - [ ] 5.4 Implement provider configuration block generation + - [x] 5.4 Implement provider configuration block generation - Generate separate provider blocks for each distinct provider used - Include platform-specific configuration (endpoints, certificate settings) - _Requirements: 5.4_ - - [ ] 5.5 Implement multi-provider resource merging with conflict resolution + - [x] 5.5 Implement multi-provider resource merging with conflict resolution - Merge resources from multiple scan profiles into unified inventory - Resolve naming conflicts by prefixing with provider identifier - Preserve provider-specific attributes - _Requirements: 5.3_ - - [ ]* 5.6 Write property tests for Code Generator (Properties 10, 11, 12, 13, 14, 15) + - [x] 5.6 Write property tests for Code Generator (Properties 10, 11, 12, 13, 14, 15) - **Property 10: References in generated output use Terraform syntax** - **Property 11: Generated HCL syntactic validity** - **Property 12: File organization by resource type** @@ -179,8 +179,8 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu - **Property 15: Traceability comments in generated code** - **Validates: Requirements 2.2, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6** -- [ ] 6. Implement State Builder - - [ ] 6.1 Implement Terraform state file generation (format v4) +- [x] 6. Implement State Builder + - [x] 6.1 Implement Terraform state file generation (format v4) - Create `StateBuilder` class - Generate state JSON with version=4, unique UUID lineage, serial number - Create state entries binding each resource block to its live infrastructure ID @@ -190,22 +190,22 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu - Include dependency references in state entries - _Requirements: 4.1, 4.2, 4.4, 4.5_ - - [ ] 6.2 Implement unmapped resource handling in state builder + - [x] 6.2 Implement unmapped resource handling in state builder - Log warnings for resources that cannot be mapped to state entries - Handle missing provider-assigned resource identifiers - Exclude unmapped resources from state file - _Requirements: 4.3, 4.6_ - - [ ]* 6.3 Write property tests for State Builder (Properties 16, 17) + - [x] 6.3 Write property tests for State Builder (Properties 16, 17) - **Property 16: State file structural validity** - **Property 17: State entry completeness and schema correctness** - **Validates: Requirements 4.1, 4.2, 4.4, 4.5** -- [ ] 7. Checkpoint - Ensure all tests pass +- [x] 7. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. -- [ ] 8. Implement Validator - - [ ] 8.1 Implement Terraform validation runner +- [x] 8. Implement Validator + - [x] 8.1 Implement Terraform validation runner - Create `Validator` class - Run `terraform init` and `terraform validate` against generated output - Run `terraform plan` and check for zero planned changes @@ -214,46 +214,46 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu - Handle missing Terraform binary with descriptive error - _Requirements: 7.1, 7.2, 7.3, 7.5_ - - [ ] 8.2 Implement auto-correction loop for validation errors + - [x] 8.2 Implement auto-correction loop for validation errors - Attempt to correct validation errors (up to 3 attempts) - Re-validate after each correction - Report failure with remaining error details if corrections exhausted - _Requirements: 7.4_ - - [ ]* 8.3 Write property test for drift report correctness (Property 22) + - [x] 8.3 Write property test for drift report correctness (Property 22) - **Property 22: Drift report correctness** - **Validates: Requirements 7.3** -- [ ] 9. Implement Incremental Scan Engine - - [ ] 9.1 Implement scan snapshot storage and retrieval +- [x] 9. Implement Incremental Scan Engine + - [x] 9.1 Implement scan snapshot storage and retrieval - Store scan results as timestamped JSON in `.iac-reverse/snapshots/` - Use profile_hash for matching scans to profiles - Retain at least 2 most recent snapshots per profile - Load previous snapshot for comparison - _Requirements: 8.4, 8.6_ - - [ ] 9.2 Implement change detection and classification + - [x] 9.2 Implement change detection and classification - Compare current scan against previous snapshot - Classify resources as added, removed, or modified - Produce change summary with counts and resource details - Handle first scan (no previous) as full initial scan - _Requirements: 8.1, 8.4, 8.5_ - - [ ] 9.3 Implement incremental code and state updates + - [x] 9.3 Implement incremental code and state updates - Update only IaC files containing changed resources (not full regeneration) - Remove resource blocks and state entries for removed resources - Add/update blocks for added/modified resources - _Requirements: 8.2, 8.3_ - - [ ]* 9.4 Write property tests for Incremental Scan (Properties 23, 24, 25, 26) + - [x] 9.4 Write property tests for Incremental Scan (Properties 23, 24, 25, 26) - **Property 23: Change classification correctness** - **Property 24: Incremental update scope** - **Property 25: Removed resource exclusion** - **Property 26: Snapshot retention** - **Validates: Requirements 8.1, 8.2, 8.3, 8.5, 8.6** -- [ ] 10. Implement CLI and wire pipeline together - - [ ] 10.1 Implement CLI entry point with Click +- [x] 10. Implement CLI and wire pipeline together + - [x] 10.1 Implement CLI entry point with Click - Create `cli.py` with Click command group - Implement `scan` command accepting scan profile YAML path - Implement `generate` command to run full pipeline (scan → resolve → generate → state → validate) @@ -264,32 +264,32 @@ Build a Python CLI tool that reverse-engineers existing on-premises infrastructu - Add progress bars and formatted output for scan progress - _Requirements: 1.1, 1.5, 6.1, 6.2, 6.3, 6.4, 6.5_ - - [ ] 10.2 Implement scan profile YAML loading and environment variable expansion + - [x] 10.2 Implement scan profile YAML loading and environment variable expansion - Parse YAML scan profiles - Expand `${ENV_VAR}` references in credential fields - Support multi-profile YAML for multi-provider scans - _Requirements: 6.1, 5.3_ - - [ ]* 10.3 Write property tests for multi-provider and filtering (Properties 18, 19, 20, 21) + - [x] 10.3 Write property tests for multi-provider and filtering (Properties 18, 19, 20, 21) - **Property 18: Multi-provider merge with naming conflict resolution** - **Property 19: Provider block generation** - **Property 20: Scan profile validation completeness** (additional coverage) - **Property 21: Filtering correctness** - **Validates: Requirements 5.3, 5.4, 6.1, 6.2, 6.4, 6.6, 6.7** -- [ ] 11. Implement resource type filter and multi-provider failure handling - - [ ] 11.1 Implement resource type filtering in scanner +- [x] 11. Implement resource type filter and multi-provider failure handling + - [x] 11.1 Implement resource type filtering in scanner - When filters specified, discover only listed resource types - When no filters specified, discover all supported types for provider - _Requirements: 6.2, 6.3_ - - [ ] 11.2 Implement multi-provider partial failure handling + - [x] 11.2 Implement multi-provider partial failure handling - Complete scanning for all remaining providers when one fails - Include successfully discovered resources in inventory - Report which providers failed with error details - _Requirements: 5.5_ -- [ ] 12. Final checkpoint - Ensure all tests pass +- [x] 12. Final checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. ## Notes diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b6393be --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools>=68.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "iac-reverse" +version = "0.1.0" +description = "Reverse engineer existing on-premises infrastructure into Terraform HCL code and state files" +readme = "README.md" +requires-python = ">=3.11" +license = {text = "MIT"} + +dependencies = [ + "click>=8.1.7", + "jinja2>=3.1.3", + "networkx>=3.2.1", + "pyyaml>=6.0.1", + "kubernetes>=28.1.0", + "docker>=7.0.0", + "pywinrm>=0.4.3", + "python-synology>=1.0.0", +] + +[project.optional-dependencies] +dev = [ + "hypothesis>=6.92.0", + "pytest>=7.4.4", + "pytest-cov>=4.1.0", +] + +[project.scripts] +iac-reverse = "iac_reverse.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] + +[tool.hypothesis] +max_examples = 100 diff --git a/src/iac_reverse.egg-info/PKG-INFO b/src/iac_reverse.egg-info/PKG-INFO new file mode 100644 index 0000000..655bf50 --- /dev/null +++ b/src/iac_reverse.egg-info/PKG-INFO @@ -0,0 +1,23 @@ +Metadata-Version: 2.4 +Name: iac-reverse +Version: 0.1.0 +Summary: Reverse engineer existing on-premises infrastructure into Terraform HCL code and state files +License: MIT +Requires-Python: >=3.11 +Description-Content-Type: text/markdown +Requires-Dist: click>=8.1.7 +Requires-Dist: jinja2>=3.1.3 +Requires-Dist: networkx>=3.2.1 +Requires-Dist: pyyaml>=6.0.1 +Requires-Dist: kubernetes>=28.1.0 +Requires-Dist: docker>=7.0.0 +Requires-Dist: pywinrm>=0.4.3 +Requires-Dist: python-synology>=1.0.0 +Provides-Extra: dev +Requires-Dist: hypothesis>=6.92.0; extra == "dev" +Requires-Dist: pytest>=7.4.4; extra == "dev" +Requires-Dist: pytest-cov>=4.1.0; extra == "dev" + +# SnarfCode +# I added this line to test the syncing + diff --git a/src/iac_reverse.egg-info/SOURCES.txt b/src/iac_reverse.egg-info/SOURCES.txt new file mode 100644 index 0000000..fe248d7 --- /dev/null +++ b/src/iac_reverse.egg-info/SOURCES.txt @@ -0,0 +1,17 @@ +README.md +pyproject.toml +src/iac_reverse/__init__.py +src/iac_reverse.egg-info/PKG-INFO +src/iac_reverse.egg-info/SOURCES.txt +src/iac_reverse.egg-info/dependency_links.txt +src/iac_reverse.egg-info/entry_points.txt +src/iac_reverse.egg-info/requires.txt +src/iac_reverse.egg-info/top_level.txt +src/iac_reverse/auth/__init__.py +src/iac_reverse/cli/__init__.py +src/iac_reverse/generator/__init__.py +src/iac_reverse/incremental/__init__.py +src/iac_reverse/resolver/__init__.py +src/iac_reverse/scanner/__init__.py +src/iac_reverse/state_builder/__init__.py +src/iac_reverse/validator/__init__.py \ No newline at end of file diff --git a/src/iac_reverse.egg-info/dependency_links.txt b/src/iac_reverse.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/iac_reverse.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/iac_reverse.egg-info/entry_points.txt b/src/iac_reverse.egg-info/entry_points.txt new file mode 100644 index 0000000..0a76ac1 --- /dev/null +++ b/src/iac_reverse.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +iac-reverse = iac_reverse.cli:main diff --git a/src/iac_reverse.egg-info/requires.txt b/src/iac_reverse.egg-info/requires.txt new file mode 100644 index 0000000..cd3ac0d --- /dev/null +++ b/src/iac_reverse.egg-info/requires.txt @@ -0,0 +1,13 @@ +click>=8.1.7 +jinja2>=3.1.3 +networkx>=3.2.1 +pyyaml>=6.0.1 +kubernetes>=28.1.0 +docker>=7.0.0 +pywinrm>=0.4.3 +python-synology>=1.0.0 + +[dev] +hypothesis>=6.92.0 +pytest>=7.4.4 +pytest-cov>=4.1.0 diff --git a/src/iac_reverse.egg-info/top_level.txt b/src/iac_reverse.egg-info/top_level.txt new file mode 100644 index 0000000..7b96f56 --- /dev/null +++ b/src/iac_reverse.egg-info/top_level.txt @@ -0,0 +1 @@ +iac_reverse diff --git a/src/iac_reverse/__init__.py b/src/iac_reverse/__init__.py new file mode 100644 index 0000000..fe481f6 --- /dev/null +++ b/src/iac_reverse/__init__.py @@ -0,0 +1,58 @@ +"""IaC Reverse Engineering Tool. + +Reverse engineer existing on-premises infrastructure into Terraform HCL code and state files. +""" + +__version__ = "0.1.0" + +from iac_reverse.models import ( + ChangeType, + ChangeSummary, + CodeGenerationResult, + CpuArchitecture, + DependencyGraph, + DiscoveredResource, + ExtractedVariable, + GeneratedFile, + PlannedChange, + PlatformCategory, + PROVIDER_PLATFORM_MAP, + ProviderType, + ResourceChange, + ResourceRelationship, + ScanProfile, + ScanProgress, + ScanResult, + StateEntry, + StateFile, + UnresolvedReference, + ValidationError, + ValidationResult, +) +from iac_reverse.plugin_base import ProviderPlugin + +__all__ = [ + "ChangeType", + "ChangeSummary", + "CodeGenerationResult", + "CpuArchitecture", + "DependencyGraph", + "DiscoveredResource", + "ExtractedVariable", + "GeneratedFile", + "PlannedChange", + "PlatformCategory", + "PROVIDER_PLATFORM_MAP", + "ProviderPlugin", + "ProviderType", + "ResourceChange", + "ResourceRelationship", + "ScanProfile", + "ScanProgress", + "ScanResult", + "StateEntry", + "StateFile", + "UnresolvedReference", + "ValidationError", + "ValidationResult", +] diff --git a/src/iac_reverse/__pycache__/__init__.cpython-313.pyc b/src/iac_reverse/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b5be259a2d2905590007c75f3a91ee782e31567 GIT binary patch literal 1147 zcmbV~%Wl(95Qgnsn$)@9)55}HmqkMnJ3vTPnwFlnG{mK=NY<%6NsOH1XzWyNC8Y8Y zJOobw&vLe`*cB?&Epu`a-LSz@KAnj@Gygwl?s7Rpqvz|l!iRWL)4r=<^~9EkyBj)u z)+`M*OGn*`S@FXdi({N6Fu{_TWNWy_QkY_COtW=dX9gN9gBg~^EX!ez<)@(B2 zcqOYHDzFa%kVjr1`Vh|iNESNN*$z5mFM@ezS=fas2p5ENwU2x{mgQY9bON#mF69I> zbU>E6ml1L&0{4ug$IpH0wk!?q9x0Q#p3j8ZI66q*p@QjrM)o-mVeVI{=Du&+ zU$i^&p~IpvJ(qJ>f;R-JV{_^egf-^WilCb;9nwi+A6q=O(2V{Om9smFU literal 0 HcmV?d00001 diff --git a/src/iac_reverse/__pycache__/models.cpython-313.pyc b/src/iac_reverse/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea551fa52c20f25bfe20df7b00b492daad5dbd54 GIT binary patch literal 15440 zcmcJ0du&@*dgmp1`6fk4)cbiYKWWRfW69 w#G9v?IroTvE<72xl0YypkAGB3Zqk}4~&d9jRLf&r`yG3fL&nPqCghg-A?C^V*Y4RvKm;-Z3a+LV6iH&026sT zeW1YpzH=X>Br45L(F^PFob#P~?m6H0o$q

-T#EJiq=w18@ED3xe>s)G!{uUfEq2 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 Fehbdi!Vv%f literal 0 HcmV?d00001 diff --git a/src/iac_reverse/auth/__init__.py b/src/iac_reverse/auth/__init__.py new file mode 100644 index 0000000..5c15d92 --- /dev/null +++ b/src/iac_reverse/auth/__init__.py @@ -0,0 +1,21 @@ +"""Authentication module for Authentik SSO integration.""" + +from iac_reverse.auth.authentik_auth import ( + AuthenticationError, + AuthentikAuthProvider, + AuthentikConfig, + AuthentikSession, +) +from iac_reverse.auth.authentik_discovery import ( + AuthentikDiscoveryError, + AuthentikDiscoveryPlugin, +) + +__all__ = [ + "AuthenticationError", + "AuthentikAuthProvider", + "AuthentikConfig", + "AuthentikSession", + "AuthentikDiscoveryError", + "AuthentikDiscoveryPlugin", +] diff --git a/src/iac_reverse/auth/__pycache__/__init__.cpython-313.pyc b/src/iac_reverse/auth/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b6cb0656aa2b589c9518de29a58370bc3c8fe69 GIT binary patch literal 565 zcmaJ;%SyvQ6rIVViS^Nfh`P}%++vbe!_oZ=bY6!ilqzakOqdbw@6Y>!tANyJl zdm3n-4!UQ6;hA7Mir0HJ&jQP{!FJFg@{KDDD~ajW9*)n)#SP607KH`lnRCbEaY~&b z2dAox9N+IdEGuXN^QP9jvUUxzS+y)KcL6wvt%@9}!vOe%#S}ziNq2ZQWQk}l37_V< z^ma9|z2q)f9`Ol<=|XhpcVUo@6PCF((B&{-C@>Y&6j%yu372kNScJqpB1G)7Fd{&u zY))IDoNf;4^&_Ivzof*$KbPWa%5OvE7N#j7 CNE~(A!F6}g5Bw~IVTZ>w$RP@T zs0PYAAcSSCAf$Ca753@kAI_jDC9quio`eq_WBiKRGsDDMi6qR{bgX^WtyS&Of(_a7 E1sLF_IsgCw literal 0 HcmV?d00001 diff --git a/src/iac_reverse/auth/__pycache__/authentik_auth.cpython-313.pyc b/src/iac_reverse/auth/__pycache__/authentik_auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3df61785031ea03320b07fa96c2203f7c972ea3 GIT binary patch literal 7647 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 literal 0 HcmV?d00001 diff --git a/src/iac_reverse/auth/__pycache__/authentik_discovery.cpython-313.pyc b/src/iac_reverse/auth/__pycache__/authentik_discovery.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2aa19ebdf0e6a220be228813c84e76a853da1d6d GIT binary patch literal 13286 zcmcgzZEzdMc|Lp+IFbNJf&@QA^7tW(kSKt*WJ{tf+cZf>mMl@`35^|bf}Vh*1PcU^ zJ3tm0*YS@`hMA^~mBbZ2{Sl_kOsL6B!c3#~U-0ti zdr02n4ss%=aH37LpSPW}D|UgN9p@eAoQjh@JI}im7xG-^-RGK=rgI*}bIz-H&+!U> zu32e5*P^tra_;l3=X{D!;JUeCPHgJsM9+f%X@}md5)i#2FE)!UVyoyA{bE3D6Wb$R zrEQ^op<|)rxZQ<<3qi5t5blgZ5EMEWd?tm?YtC@j%0EpN^4H}|K6OJ#rqo3CrmQRr zxpd)LDih)PnQE3Q)QTmtnZ?w#f+FQp*^DYks*uVoDw3L43W0Dt- zNnB6mW!77*-58^soUe9V3tUP|`NgcVG>w+7WtC;EdP405hvsH(=( z8|oI)c{JuTN?u}2Oi+J}%T|4O)QyMtxCKtJL0IgfO>u~J#VI-z7beA}g-mW6om-w& zl&tcA>a84~mr|;n6yC(VS>~lSeR3v|EoAcGE_frzR5NmyqtScf@r<-2$Kx6wk1u7D zg)}|4#N(eUNa^Yqe>}dJQq+7pm60=9d~S-zli36wI^%K7Up|$HOZmK#x>~?A#N&z| zoq9wg)6Ps!F3itfnp+SvlCn6RP09z=7qKX&Kf2Oc%`?S~ zW&f(Dnw`iha?~n+(UsonCqoXcEgYy(W0GkIrry@DK~!1J<+D+Xtlp59a}rn@UFk5& zP{X?X)-tOy%&6V&LtYF=C8f8A!_Zl3}bv+XD!b%F0HMWT^*R#TgyUiHlBEO zKvpW2VOV>>`mP0PH1y7EUGe!@aV|DLJsZFL`lZ?U)TNi>7p5-Ntpb`@x*knnJv3Ma zy`rm^d&3EkbT7CUniiULFcTf3^SG0M5Z^qaW5IjeE_&D-kGJN9CW1WVd0AddT^`T! zT1AvXz7KR-SdL$8HEN1J^U1#uSZIsd&>n+P)M!8M5d*A7hu8)hI-TZ)AZybtwzF@Y z>|4izyZSwdT7X^jw@aklS}&bqC(93sU93k<=uwE}bfc7I)EG}U%kL3;lwP1ipVmEP z2!@FcI@UZG2LlTK0j?S!{%^Pq;GfQBG5|hjBT7fB6DJSirqi_{9!W&$+jO*xYS z9LvIw-}#26kNM>sv{*X5EGPwChe}jBF9CB=T~6|wbdy8sK z_-7;iUF5#U#ccED$nd3lku~;owN}QyPZ_9Svy3BVAFI|)Hgnt+YJpv%^XV#m*%ou` zQ^sW}W1lC4*mH?)qXc+w(td3s?KBDwa90ewlAAX&E%#wA#?6zy+;fRdHOlj(nD<;y zSKmdC=$){U1$oMuamJkU?RyI1RrF83#&J^p_yY!gjK6WMwg$D@Ke1NFY4BC*spEC9 zaSJ}!vBw-WieeYIXmabCZ^+8Kz~kd;;v|?88JA^NTVi%lZH>8n+%d}xi@uC&%C%@) z7$CDns}xKQn2ViRt&_sbivsnX z5E(oJ_6mzqDy;&&JO&e#FeXa#nxt1IOlp(DA*0kGA*I5<_*@|cH~@TG*}C$iwT4OP z*HzJMQH8Ka^XA}|#8n{g16tk>29Vr!Xie8;iJTP8os$%4N!6P2sU;Z})Jkp2CfQif z8)!;EIB89KTybEAFQ#sT+{$rF#~Sbfb&p~!H>Gqc$uudOwA9zB>gnI8s#;itR|G|q z7huROrBsz%v#|ZaKhR+Q5y?uwg-{_Q8)6A%o-e4v$^ju+P~c?kohN9V#Er}0X3cR; z&NJH_3I{@z$|;9IRyl%1^U#byGr<*#UoE84Nv1SfnWBgrEDX%+g=TLh)~q)k zPry%5$aD$271CGAlaw5z`a>2nn&)&5LW6hS`r2FNK=6+L zwtq`FT?#x~?mn>HeYn_txYRvf?jBixbt`nJ+}-=o>+1Hco~?9oEy3;P!D92^`ms{; z6RfqDEUmd4e8156`%0<#Nvhu--Sliv%oHbPwxY8Y&i0Zmg0wCA$l;)YIa+-co*Nut zTKAW`hqyrZwtuYXAA7_(Fa#p-)aJ>}!}q2?u-#L)0+zA=SH*!MzhZj)Z=QH&#=(8) z2+kaJd>Cq*8FPF%=E8HGeL{wp#aFB0)QLqFH%1pXMyEy0;p~;_hUMQRs-*Yc;dWcNfUxa9QS_hK@|<9%RO5Mg z5yfFWHMBKMD+hBEN?GQ~>~_s-kTb~~A~AW=9Be|Xs^b-CCc^?I7$SQY zXvzXjV04WVX#xSt5!|)u5VV(xzBFjXjfPb1kXd;z_v#CNf!Nm1*j54Cf?s|6t8bS( zI#*w&W$U}`+wu=?c?KWnAxSKgWimnD1BXQ0tHcZYN}q0VOityjM!pvsI)Av-1+)0bj`WC`L4;0sBN z*?-|WVu+97fbgb#Rj4{vr|StM8F8{qFdO73CShSD^66#18b(1RF(S;!ixQ%I%qyO{ zF!RiDa{H*^B{TSq4KUh0ApvHu81Zu3sE`LY9%b zrjvS~7}}7lp}E*+U=P!!muYfHL=A=6p$|1~<)1cga&GS$X;Au)iDVUDy1BsMwtuwf zAKmhd>T~WjhN5b|Jp$GSXPm`MfoH~nGJKKVuX zwEQXeC&Y(@@8m`7@KOjj&5B1S&zo}seH zT}oD2rB^q4Yr@2&U~K0AZeh3qo8{C^Xiw`#j8MoWDfEbu)Y%rb?HQ|uLHsjXt+OzU zJ~BGA)AAd5w=A?+^Kj*4gkL#jV(O%@RDgbvECqzCGMsYR>;c(4X2S~M^~JDL>r50B z1&X9zQENf7q`7}0C$(-PS7$ohs-;{SYAvY+^Vz(Vt|y|d9E@d5jov^)*%VC*EA7mO zuj}ZFeNxao27TtaV{gf8w6AKOHzg%Q)`;e2yHC&rDr}oLdCzeAmU3DPrg$-h-KXo4 zdR^mFY8)PRJ|(5YZOj$aT}=|L!Y69JQd|SC08i{ z@^ea(64J9;v(9MUnsTI)x3pG+DQr?Sz9wGi0ms2TD2Zz>r>4DUCqqsR5RXbve z)r_%UTXk_CFMOA%7l1@?bjM-RZu^Jv$9&22HDB?Ohx2ydk4&t3DFW*$9~fA5|7>LZ z-BW9?e(l-K(H};B5Lxx?boH!7N?qaA=ARB9*|_-8@aba9!0PmmTKaba2g-*}?1aZR zUjObu`RGJBd>l?}Tl=n~2_9uT7wG@z(aDm3vK;6p@3-7_`rd_m&u<0K?>g*l?GN2t zTTjL5X>Bj}4{i6KD)yf$2ZzwPf%bR#3O@F32ZUlkSnvHPF#a&ijXeAC8P40g)6!jj zVq)ju*hbHveikj34^E)bRv#Mm@@TYoc)RCtvFGr1Po&rr*|@bGeW4hAVLLigjLwvL zX19Chiam3so>y0WKlS&Ny9Udh{pF6Ha$mUI)3+1qU3=#1pI?9V_7@(uHjzCGn!nr~ z;e!2eh0)PK*PYhet>vzt2tQ0Ue2?rA6G9%gdF=1ApIA$YTCF6h{;}mTf7h=N%5bSECb%)6w zAZmNAm>Dz0chkP96*6MOF_#DnDqO;iVw0NK=ZP1pU z#x<-O6wzb%uZvH^Vryy`Irm06)2i{rJg~l=0B^h)Q3HGFwI0}MRUT(}SRM_mq!@45 z8L)w1Br+SQaohZa*>=Oe;WUQPH?d{%8{iIXOK{V66dcO9<{YrpoTJ=@e+RO(O1#xV zcubp1f{Ms79d^)l<)V^ZvY<2!0;svOIQo!=7DV(imC*yM(4`6@bt!DO8zusIEOpww z4wLpeBF0C(*~e6~q5St&Gx8hxFNO(W09Ng!g^<1a2HY(2T2DZ}9uSmx!XbJ1U@1byWPRd_b-lDwEQ}PlL z%?S-AX)c^LLG;*3Hx|y+&&QBAVF7)H`3mjRX|>?D!83E;>@k#Qn&rPyOLah^5c4^} z1rKi=Eq0s)mXAkPo67#cs`I|Dy*>)L?kNSrbwNmBbn{p-bixvZ^syM=NYOvCu9p0V zce;+2yN+%wZNw2bJTp)pcw&3tsp7y>Tm8o>4qNBb2qcEu5lC!nM<6kH;Le%bXTJXY z>P$J|d$Qzz7Ll1X+imY{=lwwEeSg=g z@8Jo~-@)8a<%{4*op(voN?7pdEwBmrid4U1_IWD~S97sT+Bo07Pd<((7_0xWd$ld> zv^A#b+9%(=PkvKu-?p%?Me2fG@N+#3k-ae|p>ND}#q@UPJw~b8-LmIM@ti$Z%}bqc zu&Wlso@rx0wB{?r`|=_()+{-}kPA|-{%C=|wDnLwg01orIR+B8qV!{FHPrYdFsp|! zJus#_7DTm*TntC5jU{WlU2LmNP+`2LYbV+C$_B*!HqbZKp!;St6#HNH+zriHD5R2_ zJ(bk=z?^gr3!CaVKBl>Fgdr`j@P_MfBdU;=4ttsTpm__K6n0+XsQUZJ);zGP*|9pU z?eQpbi#6h-!xmpV$Awsp^d?|#QHH~Tt-&y_Ilws`AW3Kxk{7LIW^VdC4nK&Wo|?bF zqT9-O`bZ+D{0^1qh!fCC336IYV%3cu2AH%kS@X&t(i3%j@2-yW4l=dyzLxG@R6YW6 zQon;b4E;L0*PP$sz-r-S?b-?fDDO2f+`N(#dnBL+4A~uatrpVDI`n z7{0jIo!{|%%d^q78T?`Q54!Ii`ruUQ$qOanV#zinsHsN9Ranc?*_>(R}&%~P94 zwz{4xw{`Ax4sCZvik*?2fynm2OT~egc6!Eii<9ihP#*)iW5vE>n=_@pQx7}pf}?dI zQQ|NIr%MGn#2hek-iHCf#oL*;H%rAlUTe0V_G>cE___3BdhY7syk_fRJ~zxI9K{7@WO4j9g%$5&i=CWt;}n7d z8_|Ov85NtA7$)0g)l-Qy1?9R@aYA$SQ7qD-8QzfCQ+B&+O^G8Hu z|G0Al6gf+){0~qK-fR~)G_uP7+|76Kt2~a=_8nY3XV@8^DFw$?XUqQJmw)d^f$klD z&)UIncWnC)7X1gmcd#6QLpf9o46Pr8ow3u_w>JCjicD8&YieK7q%21E{L*ANkdD}Xa1 z)(r>Bm_!_~`kf@ZTBc6eUNTxR)TORya4H$yV&NBKM}Y+u$N{bHC@}L`8`&SM%_9L> znm`Kv0;Ulc2)&!X^Y-nxHyp*#k&VkG|5IC@rx-WuwkU~*jE|#uprdSbV6D-SC<=^E z*uXc(N0=$pk%{p+U|_O83VXJ2S!d#ONmx7OvaG`%QG3cv*0l1I?f zy?bxWztEo8pGy*2F%Cl5TPbitAgt)A0 z_D|Cj&4M0tqi}#i+sq2H49I0+DqEpNZE>eM>6M~ex=9|Qr-Md9eO5_zeTjQ$=UtxN zZink|g+JjM*>G-%nq-NmT{yxu#$OSg(*bGuG%9Xheg;a&-NT*A6@UHq1Jm&0Qv z7;y0$`OWFO%ex#NE2Ay0gB#No4!6y@UAk2cylQjt_d7ysj@zdyE_y+Lsi%KUeBWDX zVy_n@aanOA{%I^2Qty?~!=pEX1 zA-D2bTc3+(W#~ROUUAU<(AB%)tZ=w(1~(Hw=-H*m%0-*kb!<(o zaJa3jyL78`xn0k#<=3Y-I@Zs=|Mtfi|8qJ=iJ9RpHm#bQ0`Zw^Y?`zd9NSE%u0{}5 z#fB$a!&4}$wOh{ONALpxIj!n5%qEgRn%Q}^Y1Ts48aeix26ix1X{N#%O31U)ed_Pi z6DbW1zao$_2#d3*1pP{Y&3nBpXr5>F-yl4v{5k4C6gX8u0%y`@`?=j^bN<4|*$(_0 zC;Wu-f9!0s*+1rx{35{Fj_tY~Ht$+$m&2_R;6fAS(2;WQljZ)C<({X@!!MNkr^+E5 rtT_s+>#%)`>soWJiR&Y4uf6Yk#PK%!ZtrE6?YXtLf5Fk6ar1ux*>#t} literal 0 HcmV?d00001 diff --git a/src/iac_reverse/auth/authentik_auth.py b/src/iac_reverse/auth/authentik_auth.py new file mode 100644 index 0000000..6283db4 --- /dev/null +++ b/src/iac_reverse/auth/authentik_auth.py @@ -0,0 +1,204 @@ +"""Authentik SSO authentication provider. + +Handles OAuth2/OIDC authentication flow with an Authentik instance, +including token refresh and validation. +""" + +from dataclasses import dataclass, field +from urllib.parse import urljoin + +import requests + + +@dataclass +class AuthentikConfig: + """Configuration for connecting to an Authentik instance.""" + + base_url: str # Authentik instance URL (e.g., "https://auth.internal.lab") + client_id: str # OAuth2 client ID for this tool + client_secret: str # OAuth2 client secret + + +@dataclass +class AuthentikSession: + """Active session from Authentik SSO authentication.""" + + access_token: str + refresh_token: str + user_id: str + groups: list[str] = field(default_factory=list) + + +class AuthenticationError(Exception): + """Raised when Authentik authentication fails.""" + + pass + + +class AuthentikAuthProvider: + """Handles SSO authentication for the tool via Authentik OAuth2/OIDC. + + Provides methods to authenticate users, refresh expired sessions, + and validate existing tokens against the Authentik instance. + """ + + def authenticate_user(self, config: AuthentikConfig) -> AuthentikSession: + """Initiate OAuth2/OIDC flow with Authentik and return a session. + + Uses the client credentials or resource owner password grant to obtain + an access token from Authentik's token endpoint. + + Args: + config: Authentik connection configuration. + + Returns: + An AuthentikSession with access/refresh tokens and user info. + + Raises: + AuthenticationError: If authentication fails for any reason. + """ + token_url = urljoin(config.base_url.rstrip("/") + "/", "application/o/token/") + + try: + response = requests.post( + token_url, + data={ + "grant_type": "client_credentials", + "client_id": config.client_id, + "client_secret": config.client_secret, + "scope": "openid profile email", + }, + timeout=30, + ) + except requests.RequestException as e: + raise AuthenticationError( + f"Authentik: failed to connect to {config.base_url} - {e}" + ) + + if response.status_code != 200: + raise AuthenticationError( + f"Authentik: authentication failed with status {response.status_code} " + f"- {response.text}" + ) + + token_data = response.json() + access_token = token_data.get("access_token", "") + refresh_token = token_data.get("refresh_token", "") + + # Fetch user info to get user_id and groups + user_id, groups = self._fetch_user_info(config.base_url, access_token) + + return AuthentikSession( + access_token=access_token, + refresh_token=refresh_token, + user_id=user_id, + groups=groups, + ) + + def refresh_session( + self, config: AuthentikConfig, session: AuthentikSession + ) -> AuthentikSession: + """Refresh an expired session token. + + Args: + config: Authentik connection configuration. + session: The current session with a valid refresh token. + + Returns: + A new AuthentikSession with refreshed tokens. + + Raises: + AuthenticationError: If the refresh fails. + """ + token_url = urljoin(config.base_url.rstrip("/") + "/", "application/o/token/") + + try: + response = requests.post( + token_url, + data={ + "grant_type": "refresh_token", + "refresh_token": session.refresh_token, + "client_id": config.client_id, + "client_secret": config.client_secret, + }, + timeout=30, + ) + except requests.RequestException as e: + raise AuthenticationError( + f"Authentik: failed to refresh session - {e}" + ) + + if response.status_code != 200: + raise AuthenticationError( + f"Authentik: token refresh failed with status {response.status_code} " + f"- {response.text}" + ) + + token_data = response.json() + access_token = token_data.get("access_token", "") + refresh_token = token_data.get("refresh_token", session.refresh_token) + + user_id, groups = self._fetch_user_info(config.base_url, access_token) + + return AuthentikSession( + access_token=access_token, + refresh_token=refresh_token, + user_id=user_id, + groups=groups, + ) + + def validate_token(self, config: AuthentikConfig, token: str) -> bool: + """Validate an existing token is still valid. + + Checks the token against Authentik's userinfo endpoint. + + Args: + config: Authentik connection configuration. + token: The access token to validate. + + Returns: + True if the token is valid, False otherwise. + """ + userinfo_url = urljoin( + config.base_url.rstrip("/") + "/", "application/o/userinfo/" + ) + + try: + response = requests.get( + userinfo_url, + headers={"Authorization": f"Bearer {token}"}, + timeout=10, + ) + return response.status_code == 200 + except requests.RequestException: + return False + + def _fetch_user_info( + self, base_url: str, access_token: str + ) -> tuple[str, list[str]]: + """Fetch user info from Authentik's userinfo endpoint. + + Args: + base_url: Authentik instance base URL. + access_token: Valid access token. + + Returns: + Tuple of (user_id, groups list). + """ + userinfo_url = urljoin(base_url.rstrip("/") + "/", "application/o/userinfo/") + + try: + response = requests.get( + userinfo_url, + headers={"Authorization": f"Bearer {access_token}"}, + timeout=10, + ) + if response.status_code == 200: + data = response.json() + user_id = data.get("sub", "") + groups = data.get("groups", []) + return user_id, groups + except requests.RequestException: + pass + + return "", [] diff --git a/src/iac_reverse/auth/authentik_discovery.py b/src/iac_reverse/auth/authentik_discovery.py new file mode 100644 index 0000000..cbab560 --- /dev/null +++ b/src/iac_reverse/auth/authentik_discovery.py @@ -0,0 +1,384 @@ +"""Authentik discovery plugin. + +Discovers Authentik configurations as infrastructure resources, including +flows, stages, providers, applications, outposts, property mappings, +certificates, groups, and sources. +""" + +from typing import Callable +from urllib.parse import urljoin + +import requests + +from iac_reverse.models import ( + CpuArchitecture, + DiscoveredResource, + PlatformCategory, + ProviderType, + ScanProgress, + ScanResult, +) +from iac_reverse.plugin_base import ProviderPlugin + + +class AuthentikDiscoveryError(Exception): + """Raised when Authentik discovery encounters an error.""" + + pass + + +# Mapping of resource types to their Authentik API endpoints +_RESOURCE_TYPE_API_MAP: dict[str, str] = { + "authentik_flow": "api/v3/flows/instances/", + "authentik_stage": "api/v3/stages/all/", + "authentik_provider": "api/v3/providers/all/", + "authentik_application": "api/v3/core/applications/", + "authentik_outpost": "api/v3/outposts/instances/", + "authentik_property_mapping": "api/v3/propertymappings/all/", + "authentik_certificate": "api/v3/crypto/certificatekeypairs/", + "authentik_group": "api/v3/core/groups/", + "authentik_source": "api/v3/sources/all/", +} + + +class AuthentikDiscoveryPlugin(ProviderPlugin): + """Discovers Authentik configurations as infrastructure resources. + + Connects to an Authentik instance via its REST API and enumerates + flows, stages, providers, applications, outposts, property mappings, + certificates, groups, and sources. + + Since Authentik is an identity provider (not a traditional infrastructure + platform), it uses PlatformCategory.CONTAINER_ORCHESTRATION as a + categorization convenience — Authentik typically runs as a containerized + service within the orchestration layer. + """ + + def __init__(self) -> None: + self._base_url: str = "" + self._api_token: str = "" + self._authenticated: bool = False + + def authenticate(self, credentials: dict[str, str]) -> None: + """Authenticate with the Authentik REST API. + + Expected credentials: + - base_url: Authentik instance URL (e.g., "https://auth.internal.lab") + - api_token: Authentik API token for administrative access + + Args: + credentials: Dictionary with base_url and api_token. + + Raises: + AuthentikDiscoveryError: If authentication fails. + """ + base_url = credentials.get("base_url", "") + api_token = credentials.get("api_token", "") + + if not base_url: + raise AuthentikDiscoveryError( + "Authentik: 'base_url' is required in credentials" + ) + if not api_token: + raise AuthentikDiscoveryError( + "Authentik: 'api_token' is required in credentials" + ) + + self._base_url = base_url.rstrip("/") + self._api_token = api_token + + # Verify connectivity by hitting the core API + try: + response = requests.get( + self._build_url("api/v3/core/applications/"), + headers=self._auth_headers(), + params={"page_size": 1}, + timeout=30, + ) + except requests.RequestException as e: + raise AuthentikDiscoveryError( + f"Authentik: failed to connect to {base_url} - {e}" + ) + + if response.status_code == 401: + raise AuthentikDiscoveryError( + "Authentik: authentication failed - invalid API token" + ) + if response.status_code == 403: + raise AuthentikDiscoveryError( + "Authentik: authentication failed - insufficient permissions" + ) + if response.status_code not in (200, 201): + raise AuthentikDiscoveryError( + f"Authentik: unexpected status {response.status_code} " + f"during authentication check" + ) + + self._authenticated = True + + def get_platform_category(self) -> PlatformCategory: + """Return the platform category for Authentik. + + Authentik is an identity provider that typically runs as a containerized + service, so it is categorized under CONTAINER_ORCHESTRATION. + """ + return PlatformCategory.CONTAINER_ORCHESTRATION + + def list_endpoints(self) -> list[str]: + """Return the Authentik instance endpoint. + + Returns: + List containing the configured Authentik base URL. + """ + if not self._base_url: + return [] + return [self._base_url] + + def list_supported_resource_types(self) -> list[str]: + """Return all Authentik resource types this plugin can discover. + + Returns: + List of Authentik resource type strings. + """ + return [ + "authentik_flow", + "authentik_stage", + "authentik_provider", + "authentik_application", + "authentik_outpost", + "authentik_property_mapping", + "authentik_certificate", + "authentik_group", + "authentik_source", + ] + + def detect_architecture(self, endpoint: str) -> CpuArchitecture: + """Detect the CPU architecture of the Authentik host. + + Authentik is a web service; architecture detection is not directly + applicable. Defaults to AMD64 as the most common deployment target. + + Args: + endpoint: The Authentik endpoint URL. + + Returns: + CpuArchitecture.AMD64 as the default. + """ + return CpuArchitecture.AMD64 + + def discover_resources( + self, + endpoints: list[str], + resource_types: list[str], + progress_callback: Callable[[ScanProgress], None], + ) -> ScanResult: + """Discover Authentik resources via the REST API. + + Connects to the Authentik API and enumerates all resources of the + requested types. Reports progress via the callback function. + + Args: + endpoints: List of Authentik endpoint URLs (typically one). + resource_types: List of resource type strings to discover. + progress_callback: Callable that receives ScanProgress updates. + + Returns: + ScanResult containing all discovered Authentik resources. + + Raises: + AuthentikDiscoveryError: If not authenticated. + """ + if not self._authenticated: + raise AuthentikDiscoveryError( + "Authentik: must authenticate before discovering resources" + ) + + import datetime + + resources: list[DiscoveredResource] = [] + warnings: list[str] = [] + errors: list[str] = [] + + endpoint = endpoints[0] if endpoints else self._base_url + total_types = len(resource_types) + + for idx, resource_type in enumerate(resource_types): + progress_callback( + ScanProgress( + current_resource_type=resource_type, + resources_discovered=len(resources), + resource_types_completed=idx, + total_resource_types=total_types, + ) + ) + + if resource_type not in _RESOURCE_TYPE_API_MAP: + warnings.append( + f"Unsupported Authentik resource type: {resource_type}" + ) + continue + + try: + discovered = self._discover_resource_type( + resource_type, endpoint + ) + resources.extend(discovered) + except Exception as e: + errors.append( + f"Error discovering {resource_type}: {e}" + ) + + # Final progress update + progress_callback( + ScanProgress( + current_resource_type="complete", + resources_discovered=len(resources), + resource_types_completed=total_types, + total_resource_types=total_types, + ) + ) + + scan_timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat() + + return ScanResult( + resources=resources, + warnings=warnings, + errors=errors, + scan_timestamp=scan_timestamp, + profile_hash="", + is_partial=len(errors) > 0, + ) + + def _discover_resource_type( + self, resource_type: str, endpoint: str + ) -> list[DiscoveredResource]: + """Discover all resources of a specific type from Authentik API. + + Handles pagination to retrieve all results. + + Args: + resource_type: The Authentik resource type to discover. + endpoint: The Authentik endpoint URL. + + Returns: + List of DiscoveredResource objects. + """ + api_path = _RESOURCE_TYPE_API_MAP[resource_type] + results: list[DiscoveredResource] = [] + page = 1 + + while True: + response = requests.get( + self._build_url(api_path), + headers=self._auth_headers(), + params={"page": page, "page_size": 100}, + timeout=30, + ) + + if response.status_code != 200: + raise AuthentikDiscoveryError( + f"API request failed for {resource_type}: " + f"status {response.status_code}" + ) + + data = response.json() + items = data.get("results", []) + + for item in items: + resource = self._map_to_resource(resource_type, item, endpoint) + results.append(resource) + + # Check for next page + if data.get("pagination", {}).get("next", 0) > 0: + page += 1 + else: + break + + return results + + def _map_to_resource( + self, resource_type: str, item: dict, endpoint: str + ) -> DiscoveredResource: + """Map an Authentik API response item to a DiscoveredResource. + + Args: + resource_type: The resource type string. + item: The API response dictionary for a single resource. + endpoint: The Authentik endpoint URL. + + Returns: + A DiscoveredResource instance. + """ + # Extract common fields with sensible defaults + unique_id = str(item.get("pk", item.get("uuid", item.get("id", "")))) + name = item.get("name", item.get("slug", item.get("title", unique_id))) + + return DiscoveredResource( + resource_type=resource_type, + unique_id=f"authentik/{resource_type}/{unique_id}", + name=name, + provider=ProviderType.DOCKER_SWARM, # Closest match for containerized identity provider + platform_category=PlatformCategory.CONTAINER_ORCHESTRATION, + architecture=CpuArchitecture.AMD64, + endpoint=endpoint, + attributes=item, + raw_references=self._extract_references(item), + ) + + def _extract_references(self, item: dict) -> list[str]: + """Extract references to other resources from an API item. + + Looks for common reference fields in Authentik API responses. + + Args: + item: The API response dictionary. + + Returns: + List of reference ID strings. + """ + references: list[str] = [] + + # Common reference fields in Authentik API + ref_fields = [ + "flow", + "provider", + "application", + "outpost", + "group", + "source", + "certificate", + "stages", + "policies", + ] + + for field_name in ref_fields: + value = item.get(field_name) + if value is None: + continue + if isinstance(value, str) and value: + references.append(value) + elif isinstance(value, list): + for v in value: + if isinstance(v, str) and v: + references.append(v) + + return references + + def _build_url(self, path: str) -> str: + """Build a full URL from the base URL and a relative path. + + Args: + path: Relative API path. + + Returns: + Full URL string. + """ + return urljoin(self._base_url + "/", path) + + def _auth_headers(self) -> dict[str, str]: + """Return authorization headers for API requests. + + Returns: + Dictionary with Authorization header. + """ + return {"Authorization": f"Bearer {self._api_token}"} diff --git a/src/iac_reverse/cli/__init__.py b/src/iac_reverse/cli/__init__.py new file mode 100644 index 0000000..126fb2a --- /dev/null +++ b/src/iac_reverse/cli/__init__.py @@ -0,0 +1,6 @@ +"""CLI module for command-line interface.""" + +from iac_reverse.cli.cli import cli, main +from iac_reverse.cli.profile_loader import ProfileLoader, ProfileLoaderError + +__all__ = ["cli", "main", "ProfileLoader", "ProfileLoaderError"] diff --git a/src/iac_reverse/cli/__pycache__/__init__.cpython-313.pyc b/src/iac_reverse/cli/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..589de81532503b25944e03fcce0440c73ce2387d GIT binary patch literal 409 zcmZ9I&q@O^5XO`J*HWy4z00121zq(jB0}lGZPDsp7ed(0N+i1}(<B-44N;&(|*B@qDr7r=0Jh*~Q_Wqy$JxQ)fsM2b{zO zH?-azdWjEy5pPdiJr&(HaDEqn)*%b2Z7cax8 tj4@fu8DsI}#