forward-netbox 2.2.2__tar.gz → 2.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/PKG-INFO +4 -2
  2. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/README.md +3 -1
  3. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/__init__.py +1 -1
  4. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/jobs.py +65 -5
  5. forward_netbox-2.2.4/forward_netbox/migrations/0030_forwarddeviceanalysis_cve_ids.py +18 -0
  6. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/models.py +3 -0
  7. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_device_analysis.nqe +6 -5
  8. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_vrfs.nqe +14 -0
  9. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardsync_health.html +3 -3
  10. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +8 -0
  11. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +2 -1
  12. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_apply_identity_audit.py +32 -1
  13. forward_netbox-2.2.4/forward_netbox/tests/test_device_analysis_query.py +26 -0
  14. forward_netbox-2.2.4/forward_netbox/tests/test_execution_insights_delete_label.py +42 -0
  15. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_jobs.py +34 -0
  16. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_models.py +4 -0
  17. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_scope_module_ui.py +6 -0
  18. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_vsys_parent.py +19 -0
  19. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/apply_identity_audit.py +80 -7
  20. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/device_analysis.py +18 -0
  21. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/model_validation.py +6 -0
  22. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/query_binding_resolution.py +12 -0
  23. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/vsys_parent.py +32 -6
  24. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/pyproject.toml +1 -1
  25. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/LICENSE +0 -0
  26. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/api/__init__.py +0 -0
  27. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/api/serializers.py +0 -0
  28. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/api/urls.py +0 -0
  29. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/api/views.py +0 -0
  30. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/choices.py +0 -0
  31. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/exceptions.py +0 -0
  32. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/filtersets.py +0 -0
  33. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/forms.py +0 -0
  34. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/__init__.py +0 -0
  35. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/__init__.py +0 -0
  36. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
  37. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_apply_identity_audit.py +0 -0
  38. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
  39. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
  40. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
  41. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_link_vsys_parents.py +0 -0
  42. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
  43. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_primary_ip_audit.py +0 -0
  44. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
  45. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
  46. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_scope_ipam_audit.py +0 -0
  47. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
  48. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_tag_delete_eligible.py +0 -0
  49. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
  50. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0001_initial.py +0 -0
  51. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
  52. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
  53. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
  54. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
  55. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
  56. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
  57. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
  58. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
  59. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
  60. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
  61. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
  62. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
  63. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
  64. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
  65. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
  66. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
  67. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
  68. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
  69. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
  70. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
  71. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
  72. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
  73. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
  74. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
  75. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
  76. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
  77. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
  78. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/0029_forward_parent_device_cf.py +0 -0
  79. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/migrations/__init__.py +0 -0
  80. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/navigation.py +0 -0
  81. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/__init__.py +0 -0
  82. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
  83. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
  84. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
  85. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
  86. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
  87. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
  88. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
  89. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
  90. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
  91. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
  92. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
  93. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
  94. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
  95. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
  96. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
  97. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
  98. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
  99. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
  100. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_device_feature_tags.nqe +0 -0
  101. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
  102. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_device_models.nqe +0 -0
  103. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
  104. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_device_types.nqe +0 -0
  105. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
  106. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_devices.nqe +0 -0
  107. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +0 -0
  108. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
  109. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
  110. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_interfaces.nqe +0 -0
  111. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
  112. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
  113. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
  114. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
  115. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_locations.nqe +0 -0
  116. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
  117. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_modules.nqe +0 -0
  118. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
  119. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
  120. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
  121. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
  122. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_platforms.nqe +0 -0
  123. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
  124. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
  125. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
  126. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
  127. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/forward_vlans.nqe +0 -0
  128. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/queries/netbox_utilities.nqe +0 -0
  129. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/signals.py +0 -0
  130. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/static/forward_netbox/img/fn-logo.svg +0 -0
  131. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tables.py +0 -0
  132. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/template_content.py +0 -0
  133. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
  134. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardingestion.html +0 -0
  135. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
  136. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
  137. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
  138. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
  139. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
  140. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
  141. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
  142. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
  143. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/inc/brand_bar.html +0 -0
  144. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
  145. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
  146. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
  147. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
  148. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
  149. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +0 -0
  150. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
  151. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
  152. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
  153. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
  154. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
  155. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
  156. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
  157. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
  158. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templatetags/__init__.py +0 -0
  159. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
  160. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/__init__.py +0 -0
  161. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
  162. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
  163. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/scenarios.py +0 -0
  164. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_api_usage.py +0 -0
  165. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_api_views.py +0 -0
  166. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
  167. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_apply_engine.py +0 -0
  168. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_branching.py +0 -0
  169. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_brand_bar.py +0 -0
  170. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
  171. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_bulk_merge.py +0 -0
  172. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
  173. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
  174. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_device_scope_tagging.py +0 -0
  175. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_forms.py +0 -0
  176. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_forward_api.py +0 -0
  177. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_health.py +0 -0
  178. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_ingestion_merge.py +0 -0
  179. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_interface_naming.py +0 -0
  180. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_ip_address_dedup_query.py +0 -0
  181. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
  182. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_issue_rendering.py +0 -0
  183. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_job_compat.py +0 -0
  184. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_job_liveness.py +0 -0
  185. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_log_export.py +0 -0
  186. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_logging.py +0 -0
  187. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_module_readiness.py +0 -0
  188. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_module_readiness_rollup.py +0 -0
  189. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_plugin_integrations.py +0 -0
  190. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_primary_ip.py +0 -0
  191. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_primary_ip_audit.py +0 -0
  192. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
  193. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_query_binding.py +0 -0
  194. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
  195. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_query_registry.py +0 -0
  196. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
  197. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_runtime_dependency_check.py +0 -0
  198. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_scope_ipam_audit.py +0 -0
  199. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_scope_matched_tags.py +0 -0
  200. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_sensitive_content.py +0 -0
  201. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_stability_hardening.py +0 -0
  202. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_sync.py +0 -0
  203. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_sync_aci.py +0 -0
  204. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_sync_facade.py +0 -0
  205. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_sync_orchestration.py +0 -0
  206. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
  207. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_sync_state.py +0 -0
  208. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
  209. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
  210. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/urls.py +0 -0
  211. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/__init__.py +0 -0
  212. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/api_usage.py +0 -0
  213. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/apply_engine.py +0 -0
  214. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/apply_engine_bulk.py +0 -0
  215. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/apply_engine_decision.py +0 -0
  216. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/branch_budget.py +0 -0
  217. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/branching.py +0 -0
  218. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/bulk_merge.py +0 -0
  219. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/change_explainability.py +0 -0
  220. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/density_learning.py +0 -0
  221. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/direct_changes.py +0 -0
  222. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/drift_report.py +0 -0
  223. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/execution_ledger.py +0 -0
  224. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/execution_telemetry.py +0 -0
  225. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
  226. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/fetch_artifacts.py +0 -0
  227. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/forward_api.py +0 -0
  228. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/forward_api_impl.py +0 -0
  229. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/health.py +0 -0
  230. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/health_apply_fetch.py +0 -0
  231. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/health_checks.py +0 -0
  232. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/health_summary_blocks.py +0 -0
  233. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/ingestion_issues.py +0 -0
  234. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/ingestion_merge.py +0 -0
  235. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/ingestion_presentation.py +0 -0
  236. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/interface_naming.py +0 -0
  237. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/job_compat.py +0 -0
  238. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/job_liveness.py +0 -0
  239. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/json_safe.py +0 -0
  240. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/logging.py +0 -0
  241. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/merge.py +0 -0
  242. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/model_contracts.py +0 -0
  243. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/module_readiness.py +0 -0
  244. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/multi_branch_lifecycle.py +0 -0
  245. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
  246. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
  247. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/primary_ip.py +0 -0
  248. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/primary_ip_audit.py +0 -0
  249. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/query_binding.py +0 -0
  250. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/query_diagnostics.py +0 -0
  251. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/query_fetch.py +0 -0
  252. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/query_fetch_execution.py +0 -0
  253. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/query_registry.py +0 -0
  254. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/runtime_guidance.py +0 -0
  255. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/scope_ipam_audit.py +0 -0
  256. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/scope_reconciliation.py +0 -0
  257. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sensitive_content.py +0 -0
  258. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/single_branch_executor.py +0 -0
  259. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/snapshot_freshness.py +0 -0
  260. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/support_bundle_archive.py +0 -0
  261. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync.py +0 -0
  262. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_aci.py +0 -0
  263. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_cable.py +0 -0
  264. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_contracts.py +0 -0
  265. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_core_models.py +0 -0
  266. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_device.py +0 -0
  267. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_events.py +0 -0
  268. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_execution.py +0 -0
  269. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_facade.py +0 -0
  270. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_interface.py +0 -0
  271. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_inventory_module.py +0 -0
  272. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_ipam.py +0 -0
  273. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_orchestration.py +0 -0
  274. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_primitives.py +0 -0
  275. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_reporting.py +0 -0
  276. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_routing.py +0 -0
  277. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_routing_impl.py +0 -0
  278. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
  279. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
  280. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/sync_state.py +0 -0
  281. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/validation.py +0 -0
  282. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/utilities/validation_org_query.py +0 -0
  283. {forward_netbox-2.2.2 → forward_netbox-2.2.4}/forward_netbox/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forward-netbox
3
- Version: 2.2.2
3
+ Version: 2.2.4
4
4
  Summary: NetBox plugin to sync Forward data into NetBox via built-in NQE queries
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -31,7 +31,9 @@ Forward 26.6 is the baseline for async NQE.
31
31
 
32
32
  | Plugin Release | NetBox Version | Status |
33
33
  | --- | --- | --- |
34
- | `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
34
+ | `v2.2.4` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Hotfix: device-analysis NQE (bare foreach) errored refresh + CVE list; surface job errors into job.data |
35
+ | `v2.2.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.4`; Field-feedback fixes: delete-count labeling, vsys/vdom auto-link, skip empty VRFs, per-device CVE list, churn pinpoint, query-ID status clarify |
36
+ | `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.3`; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
35
37
  | `v2.2.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.2`; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
36
38
  | `v2.2.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.1`; Fix devices mis-assigned the ACI platform; link Palo vsys / Fortinet vdom firewalls to their physical chassis |
37
39
  | `v2.1.5` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.0`; Fix Prune orphans erroring on empty sites that still hold a VLAN/VM/prefix (delete only truly-empty sites) |
@@ -8,7 +8,9 @@ Forward 26.6 is the baseline for async NQE.
8
8
 
9
9
  | Plugin Release | NetBox Version | Status |
10
10
  | --- | --- | --- |
11
- | `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
11
+ | `v2.2.4` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Hotfix: device-analysis NQE (bare foreach) errored refresh + CVE list; surface job errors into job.data |
12
+ | `v2.2.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.4`; Field-feedback fixes: delete-count labeling, vsys/vdom auto-link, skip empty VRFs, per-device CVE list, churn pinpoint, query-ID status clarify |
13
+ | `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.3`; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
12
14
  | `v2.2.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.2`; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
13
15
  | `v2.2.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.1`; Fix devices mis-assigned the ACI platform; link Palo vsys / Fortinet vdom firewalls to their physical chassis |
14
16
  | `v2.1.5` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.0`; Fix Prune orphans erroring on empty sites that still hold a VLAN/VM/prefix (delete only truly-empty sites) |
@@ -8,7 +8,7 @@ class NetboxForwardConfig(PluginConfig):
8
8
  name = "forward_netbox"
9
9
  verbose_name = "Forward Field Integration"
10
10
  description = "Sync Forward data into NetBox using built-in NQE queries."
11
- version = "2.2.2"
11
+ version = "2.2.4"
12
12
  base_url = "forward"
13
13
  min_version = "4.6.3"
14
14
 
@@ -167,13 +167,15 @@ def _maybe_enqueue_backfilled_tag_refresh(sync):
167
167
 
168
168
 
169
169
  def _maybe_enqueue_vsys_parent_link(sync):
170
- """Opt-in: after a successful sync, link virtual-context firewalls (Palo vsys /
170
+ """After a successful sync, link virtual-context firewalls (Palo vsys /
171
171
  Fortinet vdom) to their physical chassis via the ``forward_parent_device``
172
- custom field. Enabled per sync with ``auto_link_vsys_parents=True`` (matches
173
- the other post-sync overlays); otherwise run ``forward_link_vsys_parents`` by
174
- hand. Non-destructive and never affects the sync result.
172
+ custom field. Non-destructive, idempotent, and never affects the sync result.
173
+
174
+ Runs by DEFAULT (unlike the opt-in overlays): a blank ``Parent Device`` on
175
+ every vsys/vdom is a confusing default, so the link auto-refreshes each sync
176
+ unless the sync explicitly opts out with ``auto_link_vsys_parents=False``.
175
177
  """
176
- if not (sync.parameters or {}).get("auto_link_vsys_parents"):
178
+ if (sync.parameters or {}).get("auto_link_vsys_parents") is False:
177
179
  return
178
180
  try:
179
181
  from django.utils.module_loading import import_string
@@ -188,6 +190,28 @@ def _maybe_enqueue_vsys_parent_link(sync):
188
190
  logger.warning("Auto vsys parent-link enqueue failed: %s", exc)
189
191
 
190
192
 
193
+ def _maybe_enqueue_auto_prune(sync):
194
+ """Opt-in: after a successful sync, run "Prune orphans" (delete out-of-scope
195
+ devices + empty orphan sites). OFF by default because it deletes NetBox data;
196
+ enable per sync with ``auto_prune_orphans=True``. The prune keeps its own
197
+ guards (refuses when the Forward scope returned 0 devices). Never lets a prune
198
+ problem affect the sync result.
199
+ """
200
+ if not (sync.parameters or {}).get("auto_prune_orphans"):
201
+ return
202
+ try:
203
+ from django.utils.module_loading import import_string
204
+
205
+ Job.enqueue(
206
+ import_string("forward_netbox.jobs.prune_forward_orphans"),
207
+ instance=sync,
208
+ user=sync.user,
209
+ name=f"{sync.name} - prune orphans (auto)",
210
+ )
211
+ except Exception as exc: # pragma: no cover - defensive
212
+ logger.warning("Auto prune-orphans enqueue failed: %s", exc)
213
+
214
+
191
215
  def sync_forwardsync(job, *args, **kwargs):
192
216
  sync = ForwardSync.objects.get(pk=job.object_id)
193
217
 
@@ -198,6 +222,7 @@ def sync_forwardsync(job, *args, **kwargs):
198
222
  _maybe_enqueue_device_analysis_refresh(sync)
199
223
  _maybe_enqueue_backfilled_tag_refresh(sync)
200
224
  _maybe_enqueue_vsys_parent_link(sync)
225
+ _maybe_enqueue_auto_prune(sync)
201
226
  job.terminate()
202
227
  except Exception as exc:
203
228
  safe_save_job_data(job, sync)
@@ -360,6 +385,13 @@ def refresh_forward_device_analysis(job, *args, **kwargs):
360
385
  job.save(update_fields=["data"])
361
386
  job.terminate()
362
387
  except Exception as exc:
388
+ # Record the failure on the job so it is visible in the UI (the Data
389
+ # panel) instead of an empty Error field with null data.
390
+ job.data = {
391
+ "error": str(exc) or exc.__class__.__name__,
392
+ "error_type": exc.__class__.__name__,
393
+ }
394
+ job.save(update_fields=["data"])
363
395
  job.terminate(status=JobStatusChoices.STATUS_ERRORED)
364
396
  if type(exc) in (SyncError, JobTimeoutException):
365
397
  logger.error(exc)
@@ -383,6 +415,13 @@ def tag_forward_backfilled_devices(job, *args, **kwargs):
383
415
  job.save(update_fields=["data"])
384
416
  job.terminate()
385
417
  except Exception as exc:
418
+ # Record the failure on the job so it is visible in the UI (the Data
419
+ # panel) instead of an empty Error field with null data.
420
+ job.data = {
421
+ "error": str(exc) or exc.__class__.__name__,
422
+ "error_type": exc.__class__.__name__,
423
+ }
424
+ job.save(update_fields=["data"])
386
425
  job.terminate(status=JobStatusChoices.STATUS_ERRORED)
387
426
  if type(exc) in (SyncError, JobTimeoutException):
388
427
  logger.error(exc)
@@ -439,6 +478,13 @@ def tag_forward_delete_eligible_ipam(job, *args, **kwargs):
439
478
  job.save(update_fields=["data"])
440
479
  job.terminate()
441
480
  except Exception as exc:
481
+ # Record the failure on the job so it is visible in the UI (the Data
482
+ # panel) instead of an empty Error field with null data.
483
+ job.data = {
484
+ "error": str(exc) or exc.__class__.__name__,
485
+ "error_type": exc.__class__.__name__,
486
+ }
487
+ job.save(update_fields=["data"])
442
488
  job.terminate(status=JobStatusChoices.STATUS_ERRORED)
443
489
  if type(exc) in (SyncError, JobTimeoutException):
444
490
  logger.error(exc)
@@ -460,6 +506,13 @@ def create_forward_module_bays(job, *args, **kwargs):
460
506
  job.save(update_fields=["data"])
461
507
  job.terminate()
462
508
  except Exception as exc:
509
+ # Record the failure on the job so it is visible in the UI (the Data
510
+ # panel) instead of an empty Error field with null data.
511
+ job.data = {
512
+ "error": str(exc) or exc.__class__.__name__,
513
+ "error_type": exc.__class__.__name__,
514
+ }
515
+ job.save(update_fields=["data"])
463
516
  job.terminate(status=JobStatusChoices.STATUS_ERRORED)
464
517
  if type(exc) in (SyncError, JobTimeoutException):
465
518
  logger.error(exc)
@@ -486,6 +539,13 @@ def forward_dependency_preview(job, *args, **kwargs):
486
539
  job.save(update_fields=["data"])
487
540
  job.terminate()
488
541
  except Exception as exc:
542
+ # Record the failure on the job so it is visible in the UI (the Data
543
+ # panel) instead of an empty Error field with null data.
544
+ job.data = {
545
+ "error": str(exc) or exc.__class__.__name__,
546
+ "error_type": exc.__class__.__name__,
547
+ }
548
+ job.save(update_fields=["data"])
489
549
  job.terminate(status=JobStatusChoices.STATUS_ERRORED)
490
550
  if type(exc) in (SyncError, JobTimeoutException):
491
551
  logger.error(exc)
@@ -0,0 +1,18 @@
1
+ # Generated by Django 6.0.6 on 2026-07-01 17:10
2
+ from django.db import migrations
3
+ from django.db import models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("forward_netbox", "0029_forward_parent_device_cf"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="forwarddeviceanalysis",
15
+ name="cve_ids",
16
+ field=models.JSONField(blank=True, default=list),
17
+ ),
18
+ ]
@@ -959,6 +959,9 @@ class ForwardDeviceAnalysis(ForwardPluginModelDocsMixin, ChangeLoggedModel):
959
959
  collection_result = models.CharField(max_length=64, blank=True, default="")
960
960
  blast_radius = models.PositiveIntegerField(default=0)
961
961
  cve_count = models.PositiveIntegerField(default=0)
962
+ # The actual confirmed-vulnerable CVE IDs behind cve_count, so the device
963
+ # panel can list them (not just the exposure number).
964
+ cve_ids = models.JSONField(default=list, blank=True)
962
965
  up_interfaces = models.PositiveIntegerField(default=0)
963
966
  detail = models.CharField(max_length=255, blank=True, default="")
964
967
  snapshot_id = models.CharField(max_length=100, blank=True, default="")
@@ -30,16 +30,17 @@ let physicalLinks =
30
30
  where i.operStatus == OperStatus.UP
31
31
  foreach l in i.links
32
32
  select l)
33
- let vulnerabilities =
34
- length(foreach c in device.cveFindings
35
- where c.isVulnerable
36
- select c.cveId)
33
+ let cveIds =
34
+ (foreach c in device.cveFindings
35
+ where c.isVulnerable
36
+ select c.cveId)
37
37
  select {
38
38
  name: device.name,
39
39
  reachable: device.snapshotInfo.result == DeviceSnapshotResult.completed,
40
40
  collection_result: toString(device.snapshotInfo.result),
41
41
  blast_radius: physicalLinks,
42
- cve_count: vulnerabilities,
42
+ cve_count: length(cveIds),
43
+ cve_ids: cveIds,
43
44
  up_interfaces: upInterfaces,
44
45
  detail: device.locationName
45
46
  }
@@ -19,6 +19,20 @@ foreach ni in device.networkInstances
19
19
  where ni.name != "default"
20
20
  where toString(ni.instanceType) != "NetworkInstanceType.DEFAULT_INSTANCE"
21
21
  where isEmpty(forward_netbox_shard_keys) || ni.name in forward_netbox_shard_keys
22
+ // Skip VRFs with no addressing: NetBox derives prefixes/IPs from subinterface
23
+ // addresses keyed by networkInstanceName, so a VRF whose subinterfaces carry no
24
+ // IPv4/IPv6 address imports as an empty VRF and re-imports every sync. Only emit
25
+ // a VRF that has at least one addressed subinterface (mirrors how the prefix/IP
26
+ // queries associate addressing to a VRF).
27
+ where !isEmpty(
28
+ foreach interface in device.interfaces
29
+ foreach subinterface in interface.subinterfaces
30
+ where isPresent(subinterface.networkInstanceName)
31
+ && subinterface.networkInstanceName == ni.name
32
+ && (!isEmpty(subinterface.ipv4.addresses)
33
+ || !isEmpty(subinterface.ipv6.addresses))
34
+ select subinterface.networkInstanceName
35
+ )
22
36
  select distinct {
23
37
  name: ni.name,
24
38
  rd: null : String,
@@ -551,11 +551,11 @@
551
551
  <td>{{ item.commit_message }}</td>
552
552
  <td>
553
553
  {% if item.severity == "pass" %}
554
- <span class="badge text-bg-success">{{ item.status }}</span>
554
+ <span class="badge text-bg-success">{{ item.status_label|default:item.status }}</span>
555
555
  {% elif item.severity == "warn" %}
556
- <span class="badge text-bg-warning">{{ item.status }}</span>
556
+ <span class="badge text-bg-warning">{{ item.status_label|default:item.status }}</span>
557
557
  {% else %}
558
- <span class="badge text-bg-secondary">{{ item.status }}</span>
558
+ <span class="badge text-bg-secondary" title="{{ item.message }}">{{ item.status_label|default:item.status }}</span>
559
559
  {% endif %}
560
560
  </td>
561
561
  <td>{{ item.message }}</td>
@@ -34,6 +34,14 @@
34
34
  {% endif %}
35
35
  </td>
36
36
  </tr>
37
+ {% if analysis.cve_ids %}
38
+ <tr>
39
+ <th scope="row">{% trans "CVEs" %}</th>
40
+ <td class="lh-lg">
41
+ {% for cve in analysis.cve_ids %}<a href="https://nvd.nist.gov/vuln/detail/{{ cve|urlencode }}" target="_blank" rel="noopener"><code>{{ cve }}</code></a>{% if not forloop.last %} {% endif %}{% endfor %}
42
+ </td>
43
+ </tr>
44
+ {% endif %}
37
45
  {% if analysis.detail %}
38
46
  <tr>
39
47
  <th scope="row">{% trans "Location" %}</th>
@@ -74,7 +74,8 @@
74
74
  </div>
75
75
  <div class="text-end text-muted small">
76
76
  <div>{{ item.execution_mode }} / {{ item.fetch_mode }}</div>
77
- <div>{{ item.row_count }} rows, {{ item.delete_count }} deletes</div>
77
+ {# delete_count is PLANNED intent from the Forward snapshot diff (rows Forward stopped reporting), computed before apply. The APPLIED delete count is the ingestion's num_deleted / change log — an item already absent from NetBox plans a delete but applies none, so these legitimately differ. Label as planned to avoid contradicting the change log. #}
78
+ <div>{{ item.row_count }} rows, {{ item.delete_count }} delete(s) planned</div>
78
79
  </div>
79
80
  </div>
80
81
  </li>
@@ -39,6 +39,36 @@ class ApplyIdentityAuditTest(TestCase):
39
39
  self.assertEqual(result["would_create_count"], 0)
40
40
  self.assertFalse(result["churn_suspect"])
41
41
 
42
+ def test_churn_pairs_names_the_object_and_diff(self):
43
+ # The 1/1 churn signature should pinpoint the SAME logical object and the
44
+ # exact key disagreement (here: underscore vs dash in the slug).
45
+ # Both keys must differ for it to be churn (a matching name would match
46
+ # on the name lookup_set): NetBox slug dc_one / name "DC 1"; Forward
47
+ # slug dc-one / name "DC One".
48
+ Site.objects.create(name="DC 1", slug="dc_one")
49
+ rows = [{"slug": "dc-one", "name": "DC One"}]
50
+ result = audit_model_identity("dcim.site", rows)
51
+ self.assertTrue(result["churn_suspect"])
52
+ self.assertEqual(len(result["churn_pairs"]), 1)
53
+ pair = result["churn_pairs"][0]
54
+ self.assertIn("dc_one", pair["netbox"])
55
+ self.assertIn("dc-one", pair["forward"])
56
+ self.assertEqual(pair["differs_on"], "underscore-vs-dash")
57
+ self.assertGreater(pair["similarity"], 0.8)
58
+
59
+ def test_device_name_churn_is_audited(self):
60
+ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer
61
+
62
+ site = Site.objects.create(name="S", slug="s")
63
+ mfr = Manufacturer.objects.create(name="M", slug="m")
64
+ dt = DeviceType.objects.create(manufacturer=mfr, model="DT", slug="dt")
65
+ role = DeviceRole.objects.create(name="R", slug="r")
66
+ Device.objects.create(name="fw01-vsys1", device_type=dt, role=role, site=site)
67
+ # Forward now emits a trailing-space variant -> name-key flip-flop.
68
+ result = audit_model_identity("dcim.device", [{"name": "fw01-vsys1 "}])
69
+ self.assertTrue(result["churn_suspect"])
70
+ self.assertEqual(result["churn_pairs"][0]["differs_on"], "whitespace")
71
+
42
72
  def test_audit_filters_to_enabled_models(self):
43
73
  Site.objects.create(name="A", slug="a")
44
74
  sync = SimpleNamespace(get_model_strings=lambda: ["dcim.site", "dcim.device"])
@@ -46,5 +76,6 @@ class ApplyIdentityAuditTest(TestCase):
46
76
  payload = audit_apply_identity(
47
77
  sync, fetch_rows=lambda s, model: canned.get(model, [])
48
78
  )
49
- self.assertEqual(payload["models_audited"], ["dcim.site"])
79
+ # dcim.device is now an audited model; with no device rows it is clean.
80
+ self.assertEqual(payload["models_audited"], ["dcim.site", "dcim.device"])
50
81
  self.assertEqual(payload["churn_suspect_models"], [])
@@ -0,0 +1,26 @@
1
+ import re
2
+
3
+ from django.test import SimpleTestCase
4
+
5
+ from forward_netbox.utilities.query_registry import read_builtin_query_source
6
+
7
+
8
+ class DeviceAnalysisQueryTest(SimpleTestCase):
9
+ """Guard the device-analysis NQE against the 2.2.3 regression: a bare
10
+ ``let x = foreach ... select ...`` is INVALID NQE — the live Forward engine
11
+ returns HTTP 400 (``extraneous input 'foreach'``) and the whole
12
+ refresh-device-analysis job errors. The local NQE linter does NOT catch this
13
+ (it reported 0 errors on the broken query), so this source-level test is the
14
+ real guard. A ``foreach`` used as a value must be wrapped in parens / a call.
15
+ """
16
+
17
+ def test_cve_ids_comprehension_is_parenthesized(self):
18
+ source = read_builtin_query_source("forward_device_analysis.nqe")
19
+ self.assertIn("(foreach c in device.cveFindings", source)
20
+
21
+ def test_no_bare_foreach_in_let_binding(self):
22
+ source = read_builtin_query_source("forward_device_analysis.nqe")
23
+ # A `let <name> =` immediately followed by a bare `foreach` is the invalid
24
+ # pattern; wrapped forms (`(foreach`, `length(foreach`) are fine.
25
+ bare = re.findall(r"let\s+\w+\s*=\s*\n\s*foreach\b", source)
26
+ self.assertEqual(bare, [], f"bare foreach in a let binding: {bare}")
@@ -0,0 +1,42 @@
1
+ from django.template.loader import render_to_string
2
+ from django.test import TestCase
3
+
4
+
5
+ class ExecutionInsightsDeleteLabelTest(TestCase):
6
+ """The per-model panel shows delete_count, which is PLANNED intent from the
7
+ Forward snapshot diff (rows Forward stopped reporting), computed before apply.
8
+ An item already absent from NetBox plans a delete but applies none, so this
9
+ number legitimately differs from the applied num_deleted / change log. It must
10
+ be labeled "planned" so it does not read as "48 deleted" and contradict an
11
+ empty change log (the sync-23 confusion).
12
+ """
13
+
14
+ def _render(self):
15
+ return render_to_string(
16
+ "forward_netbox/inc/execution_insights.html",
17
+ {
18
+ "latest_execution_insights": {
19
+ "available": True,
20
+ "top_model_results": [
21
+ {
22
+ "model": "dcim.inventoryitem",
23
+ "query_name": "Forward Inventory Items",
24
+ "execution_mode": "query_path",
25
+ "fetch_mode": "nqe_parameters",
26
+ "row_count": 61129,
27
+ "delete_count": 48,
28
+ }
29
+ ],
30
+ }
31
+ },
32
+ )
33
+
34
+ def test_delete_count_is_labeled_planned(self):
35
+ html = self._render()
36
+ self.assertIn("48 delete(s) planned", html)
37
+
38
+ def test_bare_deleted_label_is_gone(self):
39
+ # The old "{n} deletes" label read as applied and contradicted the change
40
+ # log; it must not reappear.
41
+ html = self._render()
42
+ self.assertNotIn("48 deletes", html)
@@ -64,6 +64,40 @@ class ForwardJobsTest(TestCase):
64
64
  enqueue.assert_called_once()
65
65
  self.assertEqual(enqueue.call_args.kwargs["instance"], self.sync)
66
66
 
67
+ def test_vsys_parent_link_enqueues_by_default_and_respects_opt_out(self):
68
+ from forward_netbox.jobs import _maybe_enqueue_vsys_parent_link
69
+
70
+ # DEFAULT-ON: no parameter -> the parent-link overlay enqueues (a blank
71
+ # Parent Device on every vsys/vdom is a confusing default).
72
+ with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
73
+ _maybe_enqueue_vsys_parent_link(self.sync)
74
+ enqueue.assert_called_once()
75
+ self.assertEqual(enqueue.call_args.kwargs["instance"], self.sync)
76
+
77
+ # Explicit opt-out with auto_link_vsys_parents=False -> no enqueue.
78
+ self.sync.parameters = {
79
+ **self.sync.parameters,
80
+ "auto_link_vsys_parents": False,
81
+ }
82
+ with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
83
+ _maybe_enqueue_vsys_parent_link(self.sync)
84
+ enqueue.assert_not_called()
85
+
86
+ def test_auto_prune_orphans_enqueues_only_when_enabled(self):
87
+ from forward_netbox.jobs import _maybe_enqueue_auto_prune
88
+
89
+ # OFF by default (it deletes NetBox data): no enqueue.
90
+ with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
91
+ _maybe_enqueue_auto_prune(self.sync)
92
+ enqueue.assert_not_called()
93
+
94
+ # Opt-in via auto_prune_orphans: enqueues the prune job.
95
+ self.sync.parameters = {**self.sync.parameters, "auto_prune_orphans": True}
96
+ with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
97
+ _maybe_enqueue_auto_prune(self.sync)
98
+ enqueue.assert_called_once()
99
+ self.assertIn("prune orphans", enqueue.call_args.kwargs["name"])
100
+
67
101
  def test_record_timeout_issue_creates_single_issue_per_ingestion_phase(self):
68
102
  issue_1 = record_timeout_issue(
69
103
  self.ingestion,
@@ -1604,6 +1604,10 @@ class ForwardSyncModelTest(TestCase):
1604
1604
  parameters={
1605
1605
  "snapshot_id": LATEST_PROCESSED_SNAPSHOT,
1606
1606
  "dcim.device": True,
1607
+ # Isolate this test to the recurring-reschedule enqueue; the
1608
+ # vsys/vdom parent-link overlay is default-on and would add a
1609
+ # second enqueue (covered by its own test).
1610
+ "auto_link_vsys_parents": False,
1607
1611
  },
1608
1612
  )
1609
1613
  user = get_user_model().objects.create_user(username="recurring-user")
@@ -384,6 +384,7 @@ class ScopeModuleUiTest(TestCase):
384
384
  "collection_result": "DeviceSnapshotResult.completed",
385
385
  "blast_radius": 7,
386
386
  "cve_count": 2,
387
+ "cve_ids": ["CVE-2022-5678", "CVE-2021-1234"],
387
388
  "up_interfaces": 5,
388
389
  "detail": "DC1",
389
390
  },
@@ -421,6 +422,8 @@ class ScopeModuleUiTest(TestCase):
421
422
  self.assertTrue(analysis.reachable)
422
423
  self.assertEqual(analysis.blast_radius, 7)
423
424
  self.assertEqual(analysis.cve_count, 2)
425
+ # CVE IDs stored (deduped + sorted) so the panel can list them.
426
+ self.assertEqual(analysis.cve_ids, ["CVE-2021-1234", "CVE-2022-5678"])
424
427
  self.assertEqual(analysis.up_interfaces, 5)
425
428
  self.assertEqual(analysis.collection_result, "completed")
426
429
  # The failed device surfaces the specific Forward collection error token.
@@ -442,6 +445,9 @@ class ScopeModuleUiTest(TestCase):
442
445
  )
443
446
  rendered = panel.right_page()
444
447
  self.assertIn("Forward Analysis", rendered)
448
+ # The actual CVE IDs behind the exposure count are listed + linked to NVD.
449
+ self.assertIn("CVE-2021-1234", rendered)
450
+ self.assertIn("nvd.nist.gov/vuln/detail/CVE-2022-5678", rendered)
445
451
  # Deep-link pivot into the Forward app.
446
452
  self.assertIn("Open in Forward", rendered)
447
453
  self.assertIn("https://fwd.app", rendered)
@@ -39,6 +39,25 @@ class VsysParentTest(TestCase):
39
39
  self.assertEqual(vsys.custom_field_data.get(PARENT_DEVICE_CF), chassis.pk)
40
40
  # The physical chassis itself is never linked.
41
41
  self.assertIn(chassis.custom_field_data.get(PARENT_DEVICE_CF), (None, ""))
42
+ # The context is also modeled as a VirtualDeviceContext under the chassis.
43
+ from dcim.models import VirtualDeviceContext
44
+
45
+ self.assertEqual(result["vdc_created"], 1)
46
+ self.assertTrue(
47
+ VirtualDeviceContext.objects.filter(
48
+ device=chassis, name=vsys.name, status="active"
49
+ ).exists()
50
+ )
51
+ # Re-running is idempotent — the VDC is found, not re-created.
52
+ again = link_vsys_parents(
53
+ None, fetch_rows=self._rows([(vsys.name, chassis.name)])
54
+ )
55
+ self.assertEqual(again["vdc_created"], 0)
56
+ self.assertEqual(again["vdc_existing"], 1)
57
+ self.assertEqual(
58
+ VirtualDeviceContext.objects.filter(device=chassis, name=vsys.name).count(),
59
+ 1,
60
+ )
42
61
 
43
62
  def test_orphan_parent_not_linked(self):
44
63
  # vsys present in NetBox, but its physical chassis is not collected.