forward-netbox 2.2.2__tar.gz → 2.2.3__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 (282) hide show
  1. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/PKG-INFO +3 -2
  2. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/README.md +2 -1
  3. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/__init__.py +1 -1
  4. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/jobs.py +30 -5
  5. forward_netbox-2.2.3/forward_netbox/migrations/0030_forwarddeviceanalysis_cve_ids.py +18 -0
  6. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/models.py +3 -0
  7. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_analysis.nqe +6 -5
  8. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_vrfs.nqe +14 -0
  9. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_health.html +3 -3
  10. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +8 -0
  11. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +2 -1
  12. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_apply_identity_audit.py +32 -1
  13. forward_netbox-2.2.3/forward_netbox/tests/test_execution_insights_delete_label.py +42 -0
  14. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_jobs.py +34 -0
  15. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_models.py +4 -0
  16. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_scope_module_ui.py +6 -0
  17. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_vsys_parent.py +19 -0
  18. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/apply_identity_audit.py +80 -7
  19. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/device_analysis.py +18 -0
  20. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/model_validation.py +6 -0
  21. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_binding_resolution.py +12 -0
  22. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/vsys_parent.py +32 -6
  23. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/pyproject.toml +1 -1
  24. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/LICENSE +0 -0
  25. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/api/__init__.py +0 -0
  26. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/api/serializers.py +0 -0
  27. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/api/urls.py +0 -0
  28. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/api/views.py +0 -0
  29. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/choices.py +0 -0
  30. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/exceptions.py +0 -0
  31. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/filtersets.py +0 -0
  32. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/forms.py +0 -0
  33. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/__init__.py +0 -0
  34. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/__init__.py +0 -0
  35. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
  36. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_apply_identity_audit.py +0 -0
  37. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
  38. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
  39. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
  40. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_link_vsys_parents.py +0 -0
  41. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
  42. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_primary_ip_audit.py +0 -0
  43. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
  44. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
  45. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_scope_ipam_audit.py +0 -0
  46. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
  47. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_tag_delete_eligible.py +0 -0
  48. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
  49. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0001_initial.py +0 -0
  50. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
  51. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
  52. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
  53. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
  54. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
  55. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
  56. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
  57. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
  58. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
  59. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
  60. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
  61. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
  62. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
  63. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
  64. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
  65. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
  66. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
  67. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
  68. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
  69. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
  70. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
  71. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
  72. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
  73. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
  74. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
  75. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
  76. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
  77. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0029_forward_parent_device_cf.py +0 -0
  78. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/__init__.py +0 -0
  79. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/navigation.py +0 -0
  80. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/__init__.py +0 -0
  81. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
  82. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
  83. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
  84. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
  85. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
  86. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
  87. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
  88. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
  89. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
  90. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
  91. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
  92. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
  93. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
  94. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
  95. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
  96. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
  97. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
  98. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
  99. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_feature_tags.nqe +0 -0
  100. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
  101. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_models.nqe +0 -0
  102. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
  103. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_types.nqe +0 -0
  104. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
  105. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_devices.nqe +0 -0
  106. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +0 -0
  107. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
  108. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
  109. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_interfaces.nqe +0 -0
  110. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
  111. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
  112. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
  113. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
  114. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_locations.nqe +0 -0
  115. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
  116. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_modules.nqe +0 -0
  117. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
  118. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
  119. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
  120. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
  121. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_platforms.nqe +0 -0
  122. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
  123. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
  124. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
  125. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
  126. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_vlans.nqe +0 -0
  127. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/netbox_utilities.nqe +0 -0
  128. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/signals.py +0 -0
  129. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/static/forward_netbox/img/fn-logo.svg +0 -0
  130. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tables.py +0 -0
  131. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/template_content.py +0 -0
  132. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
  133. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardingestion.html +0 -0
  134. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
  135. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
  136. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
  137. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
  138. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
  139. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
  140. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
  141. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
  142. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/brand_bar.html +0 -0
  143. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
  144. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
  145. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
  146. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
  147. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
  148. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +0 -0
  149. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
  150. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
  151. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
  152. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
  153. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
  154. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
  155. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
  156. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
  157. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templatetags/__init__.py +0 -0
  158. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
  159. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/__init__.py +0 -0
  160. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
  161. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
  162. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/scenarios.py +0 -0
  163. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_api_usage.py +0 -0
  164. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_api_views.py +0 -0
  165. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
  166. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_apply_engine.py +0 -0
  167. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_branching.py +0 -0
  168. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_brand_bar.py +0 -0
  169. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
  170. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_bulk_merge.py +0 -0
  171. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
  172. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
  173. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_device_scope_tagging.py +0 -0
  174. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_forms.py +0 -0
  175. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_forward_api.py +0 -0
  176. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_health.py +0 -0
  177. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_ingestion_merge.py +0 -0
  178. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_interface_naming.py +0 -0
  179. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_ip_address_dedup_query.py +0 -0
  180. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
  181. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_issue_rendering.py +0 -0
  182. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_job_compat.py +0 -0
  183. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_job_liveness.py +0 -0
  184. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_log_export.py +0 -0
  185. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_logging.py +0 -0
  186. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_module_readiness.py +0 -0
  187. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_module_readiness_rollup.py +0 -0
  188. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_plugin_integrations.py +0 -0
  189. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_primary_ip.py +0 -0
  190. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_primary_ip_audit.py +0 -0
  191. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
  192. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_query_binding.py +0 -0
  193. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
  194. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_query_registry.py +0 -0
  195. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
  196. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_runtime_dependency_check.py +0 -0
  197. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_scope_ipam_audit.py +0 -0
  198. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_scope_matched_tags.py +0 -0
  199. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sensitive_content.py +0 -0
  200. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_stability_hardening.py +0 -0
  201. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync.py +0 -0
  202. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_aci.py +0 -0
  203. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_facade.py +0 -0
  204. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_orchestration.py +0 -0
  205. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
  206. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_state.py +0 -0
  207. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
  208. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
  209. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/urls.py +0 -0
  210. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/__init__.py +0 -0
  211. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/api_usage.py +0 -0
  212. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/apply_engine.py +0 -0
  213. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/apply_engine_bulk.py +0 -0
  214. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/apply_engine_decision.py +0 -0
  215. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/branch_budget.py +0 -0
  216. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/branching.py +0 -0
  217. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/bulk_merge.py +0 -0
  218. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/change_explainability.py +0 -0
  219. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/density_learning.py +0 -0
  220. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/direct_changes.py +0 -0
  221. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/drift_report.py +0 -0
  222. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/execution_ledger.py +0 -0
  223. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/execution_telemetry.py +0 -0
  224. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
  225. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/fetch_artifacts.py +0 -0
  226. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/forward_api.py +0 -0
  227. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/forward_api_impl.py +0 -0
  228. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/health.py +0 -0
  229. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/health_apply_fetch.py +0 -0
  230. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/health_checks.py +0 -0
  231. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/health_summary_blocks.py +0 -0
  232. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/ingestion_issues.py +0 -0
  233. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/ingestion_merge.py +0 -0
  234. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/ingestion_presentation.py +0 -0
  235. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/interface_naming.py +0 -0
  236. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/job_compat.py +0 -0
  237. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/job_liveness.py +0 -0
  238. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/json_safe.py +0 -0
  239. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/logging.py +0 -0
  240. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/merge.py +0 -0
  241. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/model_contracts.py +0 -0
  242. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/module_readiness.py +0 -0
  243. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/multi_branch_lifecycle.py +0 -0
  244. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
  245. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
  246. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/primary_ip.py +0 -0
  247. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/primary_ip_audit.py +0 -0
  248. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_binding.py +0 -0
  249. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_diagnostics.py +0 -0
  250. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_fetch.py +0 -0
  251. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_fetch_execution.py +0 -0
  252. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_registry.py +0 -0
  253. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/runtime_guidance.py +0 -0
  254. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/scope_ipam_audit.py +0 -0
  255. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/scope_reconciliation.py +0 -0
  256. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sensitive_content.py +0 -0
  257. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/single_branch_executor.py +0 -0
  258. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/snapshot_freshness.py +0 -0
  259. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/support_bundle_archive.py +0 -0
  260. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync.py +0 -0
  261. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_aci.py +0 -0
  262. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_cable.py +0 -0
  263. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_contracts.py +0 -0
  264. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_core_models.py +0 -0
  265. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_device.py +0 -0
  266. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_events.py +0 -0
  267. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_execution.py +0 -0
  268. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_facade.py +0 -0
  269. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_interface.py +0 -0
  270. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_inventory_module.py +0 -0
  271. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_ipam.py +0 -0
  272. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_orchestration.py +0 -0
  273. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_primitives.py +0 -0
  274. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_reporting.py +0 -0
  275. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_routing.py +0 -0
  276. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_routing_impl.py +0 -0
  277. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
  278. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
  279. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_state.py +0 -0
  280. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/validation.py +0 -0
  281. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/validation_org_query.py +0 -0
  282. {forward_netbox-2.2.2 → forward_netbox-2.2.3}/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.3
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,8 @@ 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.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Field-feedback fixes: delete-count labeling, vsys/vdom auto-link, skip empty VRFs, per-device CVE list, churn pinpoint, query-ID status clarify |
35
+ | `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
36
  | `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
37
  | `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
38
  | `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,8 @@ 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.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Field-feedback fixes: delete-count labeling, vsys/vdom auto-link, skip empty VRFs, per-device CVE list, churn pinpoint, query-ID status clarify |
12
+ | `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
13
  | `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
14
  | `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
15
  | `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.3"
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)
@@ -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,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.
@@ -16,12 +16,20 @@
16
16
  # * would_delete — NetBox objects matched by no Forward row.
17
17
  # A model with would_create AND would_delete both small (often 1/1) names the
18
18
  # churning object and shows the differing key. NEVER writes anything.
19
+ import difflib
20
+
19
21
  from .apply_engine_bulk import lookup_key_from_object
20
22
  from .apply_engine_bulk import lookup_key_from_values
21
23
 
22
24
  # Identity mirrors apply_engine_bulk's simple-model specs. The Forward query for
23
25
  # each model emits the lookup fields directly (slug/name/rd/vid/site_slug); we
24
26
  # read those off the row rather than recomputing slugify.
27
+ #
28
+ # dcim.device is also audited (name-level, best-effort): it is the largest model
29
+ # and the most likely carrier of a "1 created + 1 deleted" name-key flip-flop
30
+ # (e.g. a virtual-context firewall whose Forward name drifts). It is NOT a simple
31
+ # dimension — name uniqueness is site-scoped — so a device legitimately moved
32
+ # between sites can show here; treat device pairs as a lead, not a verdict.
25
33
  SIMPLE_MODEL_IDENTITY = {
26
34
  "dcim.site": {"lookup_sets": (("slug",), ("name",)), "row": ("slug", "name")},
27
35
  "dcim.manufacturer": {
@@ -35,11 +43,13 @@ SIMPLE_MODEL_IDENTITY = {
35
43
  "dcim.platform": {"lookup_sets": (("slug",), ("name",)), "row": ("slug", "name")},
36
44
  "dcim.devicetype": {"lookup_sets": (("slug",),), "row": ("slug",)},
37
45
  "ipam.vrf": {"lookup_sets": (("rd",), ("name",)), "row": ("rd", "name")},
46
+ "dcim.device": {"lookup_sets": (("name",),), "row": ("name",)},
38
47
  }
39
48
  SIMPLE_MODELS = tuple(SIMPLE_MODEL_IDENTITY)
40
49
 
41
50
 
42
51
  def _model_class(model_string):
52
+ from dcim.models import Device
43
53
  from dcim.models import DeviceRole
44
54
  from dcim.models import DeviceType
45
55
  from dcim.models import Manufacturer
@@ -54,9 +64,58 @@ def _model_class(model_string):
54
64
  "dcim.platform": Platform,
55
65
  "dcim.devicetype": DeviceType,
56
66
  "ipam.vrf": VRF,
67
+ "dcim.device": Device,
57
68
  }[model_string]
58
69
 
59
70
 
71
+ def _pair_churn(would_create, would_delete, *, limit=10):
72
+ """Match each would_delete (NetBox stored) to its most-similar would_create
73
+ (Forward computed) so the diagnostic names the churning object and the exact
74
+ key disagreement, instead of two disconnected lists.
75
+
76
+ ``would_create`` / ``would_delete`` are lists of (key, label) tuples — key is
77
+ the raw comparable string, label is the human-readable display. Returns
78
+ ``[{netbox, forward, similarity, differs_on}]`` for the best matches.
79
+ """
80
+ pairs = []
81
+ remaining = list(would_create)
82
+ for del_key, del_label in would_delete:
83
+ best = None
84
+ best_ratio = 0.0
85
+ for idx, (cre_key, cre_label) in enumerate(remaining):
86
+ ratio = difflib.SequenceMatcher(None, del_key, cre_key).ratio()
87
+ if ratio > best_ratio:
88
+ best_ratio = ratio
89
+ best = (idx, cre_key, cre_label)
90
+ if best is None:
91
+ continue
92
+ idx, cre_key, cre_label = best
93
+ pairs.append(
94
+ {
95
+ "netbox": del_label,
96
+ "forward": cre_label,
97
+ "similarity": round(best_ratio, 3),
98
+ "differs_on": _describe_diff(del_key, cre_key),
99
+ }
100
+ )
101
+ remaining.pop(idx)
102
+ pairs.sort(key=lambda p: p["similarity"], reverse=True)
103
+ return pairs[:limit]
104
+
105
+
106
+ def _describe_diff(netbox_key, forward_key):
107
+ """Short human hint for why two near-identical keys disagree."""
108
+ if netbox_key == forward_key:
109
+ return "identical"
110
+ if netbox_key.lower() == forward_key.lower():
111
+ return "case"
112
+ if netbox_key.strip() == forward_key.strip():
113
+ return "whitespace"
114
+ if netbox_key.replace("_", "-") == forward_key.replace("_", "-"):
115
+ return "underscore-vs-dash"
116
+ return "other"
117
+
118
+
60
119
  def audit_model_identity(model_string, rows, *, sample_limit=15):
61
120
  """Compare Forward ``rows`` against NetBox objects for ``model_string`` and
62
121
  report the would-create / would-delete leftovers (the churn candidates)."""
@@ -76,7 +135,7 @@ def audit_model_identity(model_string, rows, *, sample_limit=15):
76
135
 
77
136
  netbox_keys = {lookup_set: set() for lookup_set in lookup_sets}
78
137
  netbox_count = 0
79
- would_delete = []
138
+ would_delete = [] # (primary_key, label)
80
139
  for obj in model.objects.all().iterator():
81
140
  netbox_count += 1
82
141
  obj_keys = {
@@ -98,9 +157,10 @@ def audit_model_identity(model_string, rows, *, sample_limit=15):
98
157
  key_parts = ", ".join(
99
158
  f"{field}={getattr(obj, field, None)}" for field in lookup_sets[0]
100
159
  )
101
- would_delete.append(f"{obj} ({key_parts})")
160
+ primary_key = obj_keys[lookup_sets[0]] or str(obj)
161
+ would_delete.append((str(primary_key), f"{obj} ({key_parts})"))
102
162
 
103
- would_create = []
163
+ would_create = [] # (primary_key, label)
104
164
  for row in rows:
105
165
  values = {field: (row.get(field) or None) for field in row_fields}
106
166
  matched = False
@@ -113,19 +173,32 @@ def audit_model_identity(model_string, rows, *, sample_limit=15):
113
173
  label = " / ".join(
114
174
  str(values.get(field)) for field in row_fields if values.get(field)
115
175
  )
116
- would_create.append(label or "?")
176
+ primary_key = lookup_key_from_values(
177
+ values, lookup_sets[0], model_string=model_string
178
+ )
179
+ would_create.append((str(primary_key), label or "?"))
117
180
 
181
+ churn_suspect = bool(would_create) and bool(would_delete)
118
182
  return {
119
183
  "model": model_string,
120
184
  "forward_rows": len(rows),
121
185
  "netbox_count": netbox_count,
122
186
  "would_create_count": len(would_create),
123
187
  "would_delete_count": len(would_delete),
124
- "would_create_sample": sorted(would_create)[:sample_limit],
125
- "would_delete_sample": sorted(would_delete)[:sample_limit],
188
+ "would_create_sample": sorted(label for _, label in would_create)[
189
+ :sample_limit
190
+ ],
191
+ "would_delete_sample": sorted(label for _, label in would_delete)[
192
+ :sample_limit
193
+ ],
126
194
  # The churn signature: both sides non-empty and small => one logical
127
195
  # object with two different keys (create the new, delete the orphan).
128
- "churn_suspect": bool(would_create) and bool(would_delete),
196
+ "churn_suspect": churn_suspect,
197
+ # Pinpoint: pair each stale NetBox object with the Forward row most likely
198
+ # to be the SAME logical object, and name the exact key disagreement.
199
+ "churn_pairs": (
200
+ _pair_churn(would_create, would_delete) if churn_suspect else []
201
+ ),
129
202
  }
130
203
 
131
204
 
@@ -43,6 +43,23 @@ def _coerce_int(value):
43
43
  return 0
44
44
 
45
45
 
46
+ # Bound the stored CVE-id list so a device with a very large finding set can't
47
+ # bloat the row; the count (cve_count) remains authoritative for the total.
48
+ _CVE_IDS_LIMIT = 200
49
+
50
+
51
+ def _coerce_cve_ids(value):
52
+ if not isinstance(value, (list, tuple)):
53
+ return []
54
+ seen = []
55
+ for item in value:
56
+ cve = str(item or "").strip()
57
+ if cve and cve not in seen:
58
+ seen.append(cve)
59
+ seen.sort()
60
+ return seen[:_CVE_IDS_LIMIT]
61
+
62
+
46
63
  def refresh_device_analysis(sync) -> dict:
47
64
  """Upsert ForwardDeviceAnalysis rows from the device-analysis NQE.
48
65
 
@@ -80,6 +97,7 @@ def refresh_device_analysis(sync) -> dict:
80
97
  )[:64],
81
98
  "blast_radius": _coerce_int(row.get("blast_radius")),
82
99
  "cve_count": _coerce_int(row.get("cve_count")),
100
+ "cve_ids": _coerce_cve_ids(row.get("cve_ids")),
83
101
  "up_interfaces": _coerce_int(row.get("up_interfaces")),
84
102
  "detail": str(row.get("detail") or "")[:255],
85
103
  "snapshot_id": str(snapshot_id or ""),
@@ -332,6 +332,12 @@ def clean_forward_sync(sync):
332
332
  "skip_unchanged_snapshot",
333
333
  "set_primary_ip_from_mgmt_tag",
334
334
  "diff_fallback_mode",
335
+ # Post-sync overlay toggles (opt-in, except vsys parent-link which is
336
+ # default-on and opts OUT with auto_link_vsys_parents=False).
337
+ "auto_tag_backfilled",
338
+ "auto_refresh_device_analysis",
339
+ "auto_link_vsys_parents",
340
+ "auto_prune_orphans",
335
341
  BRANCH_RUN_STATE_PARAMETER,
336
342
  MODEL_CHANGE_DENSITY_PARAMETER,
337
343
  MODEL_CHANGE_DENSITY_PROFILE_PARAMETER,
@@ -430,6 +430,15 @@ def _committed_query_source(committed_query: dict) -> str:
430
430
  return ""
431
431
 
432
432
 
433
+ # Friendly, non-alarming display labels for the raw drift status codes (the raw
434
+ # code is kept for keying/counting; only the badge text changes). "unverified"
435
+ # is expected + non-blocking for org-managed direct query IDs, so it should not
436
+ # read like an error.
437
+ _QUERY_DRIFT_STATUS_LABELS = {
438
+ "direct_query_id_unverified": "Org-managed (pinned)",
439
+ }
440
+
441
+
433
442
  def _query_drift_result(
434
443
  query_map: ForwardNQEMap,
435
444
  *,
@@ -449,6 +458,9 @@ def _query_drift_result(
449
458
  "model": query_map.model_string,
450
459
  "mode": query_map.execution_mode,
451
460
  "status": status,
461
+ "status_label": _QUERY_DRIFT_STATUS_LABELS.get(
462
+ status, status.replace("_", " ").capitalize()
463
+ ),
452
464
  "severity": severity,
453
465
  "message": message,
454
466
  "remediation": remediation,