illumio-pylo 0.3.9__tar.gz → 0.3.11__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 (111) hide show
  1. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/.github/workflows/python-publish.yml +2 -2
  2. {illumio_pylo-0.3.9/illumio_pylo.egg-info → illumio_pylo-0.3.11}/PKG-INFO +9 -3
  3. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/API/APIConnector.py +65 -8
  4. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/API/Explorer.py +44 -0
  5. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/API/JsonPayloadTypes.py +39 -0
  6. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/IPList.py +15 -8
  7. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/IPMap.py +9 -0
  8. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/__init__.py +1 -1
  9. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/__init__.py +1 -0
  10. illumio_pylo-0.3.11/illumio_pylo/cli/commands/label_delete_unused.py +82 -0
  11. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/ven_duplicate_remover.py +72 -40
  12. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11/illumio_pylo.egg-info}/PKG-INFO +9 -3
  13. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo.egg-info/SOURCES.txt +1 -0
  14. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo.egg-info/requires.txt +1 -1
  15. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/requirements.txt +1 -1
  16. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/.devcontainer/Dockerfile +0 -0
  17. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/.devcontainer/devcontainer.json +0 -0
  18. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/.gitattributes +0 -0
  19. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/.github/workflows/doxygen-publish.yml +0 -0
  20. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/.github/workflows/make-binaries.yml +0 -0
  21. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/.gitignore +0 -0
  22. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/LICENSE +0 -0
  23. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/README.md +0 -0
  24. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/check_unique_hostnames.py +0 -0
  25. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/check_unique_services.py +0 -0
  26. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/delete_all_workloads.py +0 -0
  27. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/delete_unused_services.py +0 -0
  28. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/explorer_report_exporter.py +0 -0
  29. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/export_rules_to_firewall.py +0 -0
  30. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/generate-random-workloads.py +0 -0
  31. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/healthcheck_log.py +0 -0
  32. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/import-labels.py +0 -0
  33. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/import_workloads_placeholders.py +0 -0
  34. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/iplists_stats_duplicates_unused_finder.py +0 -0
  35. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/recalculate_explorer_logs.py +0 -0
  36. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/recalculate_explorer_logs_multithreaded.py +0 -0
  37. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/rules_exporter.py +0 -0
  38. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/rules_exporter_special.py +0 -0
  39. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/test.py +0 -0
  40. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/test_change_workload_desc.py +0 -0
  41. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/test_query.py +0 -0
  42. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/test_query2.py +0 -0
  43. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/test_securityprincipals.py +0 -0
  44. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/ven_idle_to_illumination.py +0 -0
  45. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/dev_playground/ven_reassign_pce.py +0 -0
  46. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/examples/explorer_query.py +0 -0
  47. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/examples/extend_cli.py +0 -0
  48. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/API/AuditLog.py +0 -0
  49. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/API/ClusterHealth.py +0 -0
  50. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/API/CredentialsManager.py +0 -0
  51. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/API/RuleSearchQuery.py +0 -0
  52. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/API/__init__.py +0 -0
  53. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/AgentStore.py +0 -0
  54. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Exception.py +0 -0
  55. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Helpers/__init__.py +0 -0
  56. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Helpers/exports.py +0 -0
  57. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Helpers/functions.py +0 -0
  58. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Label.py +0 -0
  59. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/LabelCommon.py +0 -0
  60. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/LabelGroup.py +0 -0
  61. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/LabelStore.py +0 -0
  62. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/LabeledObject.py +0 -0
  63. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Organization.py +0 -0
  64. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Query.py +0 -0
  65. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/ReferenceTracker.py +0 -0
  66. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Rule.py +0 -0
  67. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Ruleset.py +0 -0
  68. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/RulesetStore.py +0 -0
  69. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/SecurityPrincipal.py +0 -0
  70. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Service.py +0 -0
  71. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/SoftwareVersion.py +0 -0
  72. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/VirtualService.py +0 -0
  73. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/VirtualServiceStore.py +0 -0
  74. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/Workload.py +0 -0
  75. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/WorkloadStore.py +0 -0
  76. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/WorkloadStoreSubClasses.py +0 -0
  77. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/NativeParsers.py +0 -0
  78. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/__init__.py +0 -0
  79. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/__main__.py +0 -0
  80. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/credential_manager.py +0 -0
  81. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/iplist_analyzer.py +0 -0
  82. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/iplist_import_from_file.py +0 -0
  83. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/ruleset_export.py +0 -0
  84. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/update_pce_objects_cache.py +0 -0
  85. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/utils/LabelCreation.py +0 -0
  86. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/utils/__init__.py +0 -0
  87. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/utils/misc.py +0 -0
  88. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/ven_compatibility_report_export.py +0 -0
  89. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/ven_idle_to_visibility.py +0 -0
  90. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/ven_upgrader.py +0 -0
  91. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/workload_export.py +0 -0
  92. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/workload_import.py +0 -0
  93. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/workload_reset_names_to_null.py +0 -0
  94. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/workload_update.py +0 -0
  95. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/cli/commands/workload_used_in_rule_finder.py +0 -0
  96. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/docs/Doxygen +0 -0
  97. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/tmp.py +0 -0
  98. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/utilities/__init__.py +0 -0
  99. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/utilities/cli.py +0 -0
  100. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/utilities/credentials.example.json +0 -0
  101. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/utilities/health_monitoring.py +0 -0
  102. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/utilities/resources/iplists-import-example.csv +0 -0
  103. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/utilities/resources/iplists-import-example.xlsx +0 -0
  104. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/utilities/resources/workload-exporter-filter-example.csv +0 -0
  105. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/utilities/resources/workloads-import-example.csv +0 -0
  106. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo/utilities/resources/workloads-import-example.xlsx +0 -0
  107. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo.egg-info/dependency_links.txt +0 -0
  108. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/illumio_pylo.egg-info/top_level.txt +0 -0
  109. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/pyproject.toml +0 -0
  110. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/setup.cfg +0 -0
  111. {illumio_pylo-0.3.9 → illumio_pylo-0.3.11}/setup.py +0 -0
@@ -26,10 +26,10 @@ jobs:
26
26
  python-version: '3.11'
27
27
  - name: Install dependencies
28
28
  run: |
29
- python -m pip install --upgrade pip
29
+ python -m pip install --upgrade pip setuptools wheel twine pkginfo
30
30
  pip install build
31
31
  - name: Build package
32
32
  run: python -m build
33
33
  - name: Publish package
34
- uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14
34
+ uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
35
35
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: illumio_pylo
3
- Version: 0.3.9
3
+ Version: 0.3.11
4
4
  Summary: A set of tools and library for working with Illumio PCE
5
5
  Home-page: https://github.com/cpainchaud/pylo
6
6
  Author: Christophe Painchaud
@@ -187,11 +187,17 @@ Requires-Python: >=3.11
187
187
  License-File: LICENSE
188
188
  Requires-Dist: click==8.1.7
189
189
  Requires-Dist: colorama~=0.4.4
190
- Requires-Dist: cryptography==42.0.7
190
+ Requires-Dist: cryptography==44.0.1
191
191
  Requires-Dist: openpyxl~=3.1.3
192
192
  Requires-Dist: paramiko~=3.4.0
193
193
  Requires-Dist: prettytable~=3.10.0
194
194
  Requires-Dist: requests~=2.32.0
195
195
  Requires-Dist: xlsxwriter~=3.2.0
196
+ Dynamic: author
197
+ Dynamic: author-email
198
+ Dynamic: description
199
+ Dynamic: home-page
200
+ Dynamic: license-file
201
+ Dynamic: requires-python
196
202
 
197
203
  README.md
@@ -69,14 +69,14 @@ class APIConnector:
69
69
  if type(port) is int:
70
70
  port = str(port)
71
71
  self.port: int = port
72
- self._api_key: str = api_key
73
- self._decrypted_api_key: Optional[str] = None
74
72
  self.api_user: str = api_user
73
+ self._api_key: str = api_key
74
+ self._decrypted_api_key: Optional[str] = None # if api_key is encrypted, this will hold the decrypted value after first use
75
75
  self.org_id: int = org_id
76
76
  self.skipSSLCertCheck: bool = skip_ssl_cert_check
77
77
  self.version: Optional['pylo.SoftwareVersion'] = None
78
78
  self.version_string: str = "Not Defined"
79
- self._cached_session = requests.session()
79
+ self._cached_session = requests.sessions.Session()
80
80
 
81
81
  @property
82
82
  def api_key(self):
@@ -533,13 +533,19 @@ class APIConnector:
533
533
  params = {'include_deny_rules': include_boundary_rules}
534
534
  return self.do_post_call(path='/sec_policy/draft/rule_coverage', json_arguments=data, include_org_id=True, json_output_expected=True, async_call=False, params=params)
535
535
 
536
- def objects_label_get(self, max_results: int = None, async_mode=True) -> List[LabelObjectJsonStructure]:
536
+ def objects_label_get(self, max_results: int = None, async_mode=True, get_usage: bool = False, get_deleted: bool = False) -> List[LabelObjectJsonStructure]:
537
537
  path = '/labels'
538
538
  data = {}
539
539
 
540
540
  if max_results is not None:
541
541
  data['max_results'] = max_results
542
542
 
543
+ if get_usage:
544
+ data['usage'] = 'true'
545
+
546
+ if get_deleted:
547
+ data['includeDeleted'] = 'true'
548
+
543
549
  return self.do_get_call(path=path, async_call=async_mode, params=data)
544
550
 
545
551
  def objects_label_update(self, href: str, data: LabelObjectUpdateJsonStructure):
@@ -553,6 +559,60 @@ class APIConnector:
553
559
 
554
560
  return self.do_delete_call(path=path, json_output_expected=False, include_org_id=False)
555
561
 
562
+ class LabelMultiDeleteTracker:
563
+ _errors: Dict[str, str]
564
+ _hrefs: Dict[str, bool]
565
+ _labels: Dict[str, 'pylo.Label'] # dict of workloads by HREF
566
+ connector: 'pylo.APIConnector'
567
+
568
+ def __init__(self, connector: 'pylo.APIConnector'):
569
+ self.connector = connector
570
+ self._errors: Dict[str, Union[bool, str]] = {}
571
+ self._hrefs: Dict[str, str] = {}
572
+ self._labels: Dict[str, 'pylo.Label'] = {}
573
+
574
+ def add_label(self, label_or_href: Union['pylo.Label', str]):
575
+ if type(label_or_href) is str:
576
+ self._hrefs[label_or_href] = True
577
+ return
578
+ self._labels[label_or_href.href] = label_or_href
579
+ self._hrefs[label_or_href.href] = True
580
+
581
+ def execute_deletion(self):
582
+ for href in self._hrefs.keys():
583
+ try:
584
+ self.connector.objects_label_delete(href)
585
+ self._errors[href] = False
586
+ except Exception as e:
587
+ self._errors[href] = str(e)
588
+
589
+ return self._errors
590
+
591
+ def has_errors(self) -> bool:
592
+ for href in self._errors.keys():
593
+ if self._errors[href] is not False:
594
+ return True
595
+ return False
596
+
597
+ def get_errors_count(self) -> int:
598
+ count = 0
599
+ for href in self._errors.keys():
600
+ if self._errors[href] is not False:
601
+ count += 1
602
+ return count
603
+
604
+ def get_error(self, label_or_href: Union['pylo.Label', str]) -> Optional[str]:
605
+ href = label_or_href
606
+ if type(label_or_href) is pylo.Label:
607
+ href = label_or_href.href
608
+ error = self._errors.get(href, None)
609
+ if error is None or error is False:
610
+ return None
611
+ return error
612
+
613
+ def new_tracker_for_label_multi_deletion(self) -> 'pylo.APIConnector.LabelMultiDeleteTracker':
614
+ return pylo.APIConnector.LabelMultiDeleteTracker(self)
615
+
556
616
  def objects_label_create(self, label_name: str, label_type: str):
557
617
  path = '/labels'
558
618
  data: LabelObjectCreationJsonStructure = {'key': label_type, 'value': label_name}
@@ -1378,7 +1438,4 @@ class APIConnector:
1378
1438
  def get_pce_ui_workload_url(self, href: str) -> str:
1379
1439
  # extract UUID from workload HREF:
1380
1440
  uuid = href.split('/')[-1]
1381
- return self._make_base_url('/#/workloads/' + uuid )
1382
-
1383
-
1384
-
1441
+ return self._make_base_url('/#/workloads/' + uuid)
@@ -6,6 +6,7 @@ import illumio_pylo as pylo
6
6
  from .JsonPayloadTypes import RuleCoverageQueryEntryJsonStructure
7
7
  from illumio_pylo.API.APIConnector import APIConnector
8
8
 
9
+
9
10
  class ExplorerResult:
10
11
  _draft_mode_policy_decision: Optional[Literal['allowed', 'blocked', 'blocked_by_boundary']]
11
12
  destination_workload_labels_href: List[str]
@@ -375,6 +376,15 @@ class RuleCoverageQueryManager:
375
376
  if log_id not in self.service_index_to_log_ids[service_index]:
376
377
  self.service_index_to_log_ids[service_index].append(log_id)
377
378
 
379
+ def get_allow_rules_for_log_id(self, log_id: int):
380
+ rules = []
381
+ for service_id, list_of_log_ids in self.service_index_to_log_ids.items():
382
+ if log_id in list_of_log_ids:
383
+ policy_coverage = self.service_index_policy_coverage[service_id]
384
+ for rule in policy_coverage:
385
+ rules.append(rule)
386
+ return rules
387
+
378
388
  def get_policy_decision_for_log_id(self, log_id: int) -> Optional[Literal['allowed', 'blocked', 'blocked_by_boundary']]:
379
389
  policy_decision = None
380
390
  found_boundary_block = False
@@ -406,6 +416,9 @@ class RuleCoverageQueryManager:
406
416
  def add_service(self, service_record: Dict, log_id: int):
407
417
  self.services.add_service(service_record, log_id)
408
418
 
419
+ def get_allow_rules_for_log_id(self, log_id: int) -> List[str]:
420
+ return self.services.get_allow_rules_for_log_id(log_id)
421
+
409
422
  def get_policy_decision_for_log_id(self, log_id: int) -> Optional[Literal['allowed', 'blocked', 'blocked_by_boundary']]:
410
423
  return self.services.get_policy_decision_for_log_id(log_id)
411
424
 
@@ -511,6 +524,19 @@ class RuleCoverageQueryManager:
511
524
  # print(f'Processing deny_edge {edge} against query {query.src_workload_href} -> {query.dst_workload_href} -> {len(query.services.services_array)}')
512
525
  query.process_response_boundary_deny(deny_rules, edge)
513
526
 
527
+ def get_allow_rules_for_log_id(self, log_id: int) -> List[str]:
528
+ rules = []
529
+ for query in self.queries.values():
530
+ results = query.get_allow_rules_for_log_id(log_id)
531
+ if results is not None:
532
+ for rule_dict in results:
533
+ rules.append(rule_dict['href'])
534
+
535
+ # make them unique
536
+ rules = list(set(rules))
537
+
538
+ return rules
539
+
514
540
  def get_policy_decision_for_log_id(self, log_id: int) -> Optional[Literal["allowed", "blocked", "blocked_by_boundary"]]:
515
541
  policy_decision: Optional[Literal["allowed", "blocked", "blocked_by_boundary"]] = None
516
542
  found_blocked_by_boundary = False
@@ -615,6 +641,24 @@ class RuleCoverageQueryManager:
615
641
 
616
642
  return len(_log_ids)
617
643
 
644
+ def get_allow_rules_for_log(self, log: ExplorerResult) -> List[str]:
645
+ log_id = self.log_to_id[log]
646
+ if log_id is None:
647
+ raise pylo.PyloEx('No log_id found for log', log)
648
+
649
+ rules: List[str] = []
650
+
651
+ rules.extend(self.iplist_to_workload_query_manager.get_allow_rules_for_log_id(log_id))
652
+ rules.extend(self.workload_to_iplist_query_manager.get_allow_rules_for_log_id(log_id))
653
+ rules.extend(self.workload_to_workload_query_manager.get_allow_rules_for_log_id(log_id))
654
+
655
+ # make them unique
656
+ rules = list(set(rules))
657
+
658
+ return rules
659
+
660
+
661
+
618
662
  def _get_policy_decision_for_log_id(self, log_id: int) -> Optional[Literal['allowed', 'blocked', 'blocked_by_boundary']]:
619
663
  decision = self.iplist_to_workload_query_manager.get_policy_decision_for_log_id(log_id)
620
664
  if decision == 'allowed':
@@ -25,6 +25,26 @@ class ServiceHrefRef(TypedDict):
25
25
  class VirtualServiceHrefRef(TypedDict):
26
26
  virtual_service: HrefReference
27
27
 
28
+
29
+ class LabelObjectUsageJsonStructure(TypedDict):
30
+ virtual_server: bool
31
+ label_group: bool
32
+ static_policy_scopes: bool
33
+ pairing_profile: bool
34
+ permission: bool
35
+ workload: bool
36
+ container_workload: bool
37
+ firewall_coexistence_scope: bool
38
+ containers_inherit_host_policy_scopes: bool
39
+ container_workload_profile: bool
40
+ blocked_connection_reject_scopes: bool
41
+ enforcement_boundary: bool
42
+ loopback_interfaces_in_policy_scopes: bool
43
+ ip_forwarding_enabled_scopes: bool
44
+ rule_hit_count_enabled_scopes: bool
45
+ label_mapping_rule: bool
46
+ virtual_service: bool
47
+
28
48
  class LabelObjectJsonStructure(TypedDict):
29
49
  created_at: str
30
50
  created_by: Optional[HrefReferenceWithName]
@@ -33,6 +53,7 @@ class LabelObjectJsonStructure(TypedDict):
33
53
  key: str
34
54
  updated_at: str
35
55
  updated_by: Optional[HrefReferenceWithName]
56
+ usage: Optional[LabelObjectUsageJsonStructure]
36
57
  value: str
37
58
 
38
59
 
@@ -81,6 +102,7 @@ class WorkloadInterfaceObjectJsonStructure(TypedDict):
81
102
  name: str
82
103
  address: str
83
104
 
105
+
84
106
  class WorkloadObjectJsonStructure(TypedDict):
85
107
  created_at: str
86
108
  created_by: Optional[HrefReferenceWithName]
@@ -95,6 +117,7 @@ class WorkloadObjectJsonStructure(TypedDict):
95
117
  updated_at: str
96
118
  updated_by: Optional[HrefReferenceWithName]
97
119
 
120
+
98
121
  class WorkloadObjectCreateJsonStructure(TypedDict):
99
122
  """
100
123
  This is the structure of the JSON payload for creating a workload.
@@ -106,14 +129,18 @@ class WorkloadObjectCreateJsonStructure(TypedDict):
106
129
  name: NotRequired[str]
107
130
  public_ip: NotRequired[Optional[str]]
108
131
 
132
+
109
133
  class WorkloadObjectMultiCreateJsonStructure(WorkloadObjectCreateJsonStructure):
110
134
  href: str
111
135
 
136
+
112
137
  WorkloadObjectMultiCreateJsonRequestPayload = List[WorkloadObjectMultiCreateJsonStructure]
113
138
 
139
+
114
140
  class WorkloadBulkUpdateEntryJsonStructure(WorkloadObjectCreateJsonStructure):
115
141
  href: str
116
142
 
143
+
117
144
  class WorkloadBulkUpdateResponseEntry(TypedDict):
118
145
  href: str
119
146
  status: Literal['updated', 'error', 'validation_failure']
@@ -150,11 +177,13 @@ class VenObjectJsonStructure(TypedDict):
150
177
  os_platform: Optional[str]
151
178
  uid: Optional[str]
152
179
 
180
+
153
181
  class VENUnpairApiResponseSingleErrorObjectJsonStructure(TypedDict):
154
182
  token: str
155
183
  message: str
156
184
  hrefs: List[str]
157
185
 
186
+
158
187
  class VENUnpairApiResponseObjectJsonStructure(TypedDict):
159
188
  errors: List[VENUnpairApiResponseSingleErrorObjectJsonStructure]
160
189
 
@@ -173,6 +202,7 @@ class RuleDirectServiceReferenceObjectJsonStructure(TypedDict):
173
202
  class RuleObjectJsonStructure(TypedDict):
174
203
  created_at: str
175
204
  created_by: Optional[HrefReferenceWithName]
205
+ description: str
176
206
  href: str
177
207
  ingress_services: List[RuleDirectServiceReferenceObjectJsonStructure|RuleServiceReferenceObjectJsonStructure]
178
208
  updated_at: str
@@ -219,26 +249,31 @@ class VirtualServiceObjectJsonStructure(TypedDict):
219
249
  updated_at: str
220
250
  updated_by: Optional[HrefReferenceWithName]
221
251
 
252
+
222
253
  class NetworkDeviceConfigObjectJsonStructure(TypedDict):
223
254
  device_type: Literal['switch']
224
255
  name: str
225
256
 
257
+
226
258
  class NetworkDeviceObjectJsonStructure(TypedDict):
227
259
  href: str
228
260
  config: NetworkDeviceConfigObjectJsonStructure
229
261
  supported_endpoint_type: Literal['switch_port']
230
262
 
263
+
231
264
  class NetworkDeviceEndpointConfigObjectJsonStructure(TypedDict):
232
265
  type: Literal['switch_port']
233
266
  name: str
234
267
  workload_discovery: bool
235
268
 
269
+
236
270
  class NetworkDeviceEndpointObjectJsonStructure(TypedDict):
237
271
  href: str
238
272
  config: NetworkDeviceEndpointConfigObjectJsonStructure
239
273
  status: Literal['unmonitored', 'monitored']
240
274
  workloads: List[HrefReference]
241
275
 
276
+
242
277
  class SecurityPrincipalObjectJsonStructure(TypedDict):
243
278
  created_at: str
244
279
  created_by: Optional[HrefReferenceWithName]
@@ -247,6 +282,7 @@ class SecurityPrincipalObjectJsonStructure(TypedDict):
247
282
  updated_at: str
248
283
  updated_by: Optional[HrefReferenceWithName]
249
284
 
285
+
250
286
  class LabelDimensionObjectStructure(TypedDict):
251
287
  created_at: str
252
288
  created_by: Optional[HrefReferenceWithName]
@@ -286,13 +322,16 @@ WorkloadsGetQueryLabelFilterJsonStructure = List[List[str]]
286
322
 
287
323
  AuditLogApiEventType = Literal['agent.clone_detected', 'workloads.update', 'workload.update', 'workload_interfaces.update']
288
324
 
325
+
289
326
  class AuditLogEntryJsonStructure(TypedDict):
290
327
  event_type: AuditLogApiEventType
291
328
  timestamp: str
292
329
 
330
+
293
331
  class AuditLogApiRequestPayloadStructure(TypedDict):
294
332
  pass
295
333
 
334
+
296
335
  class AuditLogApiReplyEventJsonStructure(TypedDict):
297
336
  pass
298
337
 
@@ -1,5 +1,5 @@
1
1
  from .API.JsonPayloadTypes import IPListObjectJsonStructure
2
- from illumio_pylo import log
2
+ from illumio_pylo import log, IP4Map
3
3
  from .Helpers import *
4
4
 
5
5
 
@@ -20,6 +20,7 @@ class IPList(pylo.ReferenceTracker):
20
20
  self.description = description
21
21
  self.raw_json = None
22
22
  self.raw_entries = {}
23
+ self._ip4map: IP4Map = None
23
24
 
24
25
  def count_entries(self) -> int:
25
26
  return len(self.raw_entries)
@@ -56,15 +57,21 @@ class IPList(pylo.ReferenceTracker):
56
57
  self.raw_entries[entry] = entry
57
58
 
58
59
  def get_ip4map(self) -> pylo.IP4Map:
59
- new_map = pylo.IP4Map()
60
+ return self.ip4map
60
61
 
61
- for entry in self.raw_entries:
62
- if entry[0] == '!':
63
- new_map.subtract_from_text(entry[1:], ignore_ipv6=True)
64
- else:
65
- new_map.add_from_text(entry, ignore_ipv6=True)
62
+ @property
63
+ def ip4map(self) -> pylo.IP4Map:
64
+ if self._ip4map is None:
65
+ new_map = pylo.IP4Map()
66
+ self._ip4map = new_map
67
+
68
+ for entry in self.raw_entries:
69
+ if entry[0] == '!':
70
+ new_map.subtract_from_text(entry[1:], ignore_ipv6=True)
71
+ else:
72
+ new_map.add_from_text(entry, ignore_ipv6=True)
66
73
 
67
- return new_map
74
+ return self._ip4map
68
75
 
69
76
  def get_raw_entries_as_string_list(self, separator=',') -> str:
70
77
  return pylo.string_list_to_text(self.raw_entries.values(), separator=separator)
@@ -107,6 +107,15 @@ class IP4Map:
107
107
  return True
108
108
  return False
109
109
 
110
+ def match_single_ip(self, ip: str) -> bool:
111
+ ip_object = ipaddress.IPv4Address(ip)
112
+ for entry in self._entries:
113
+ if entry[start] <= int(ip_object) <= entry[end]:
114
+ return True
115
+ if entry[start] > int(ip_object):
116
+ return False
117
+ return False
118
+
110
119
  def substract(self, another_map: 'IP4Map'):
111
120
  affected_rows = 0
112
121
  for entry in another_map._entries:
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.9"
1
+ __version__ = "0.3.11"
2
2
 
3
3
  from typing import Callable
4
4
 
@@ -32,3 +32,4 @@ from .workload_reset_names_to_null import command_object
32
32
  from .credential_manager import command_object
33
33
  from .iplist_analyzer import command_object
34
34
  from .ven_compatibility_report_export import command_object
35
+ from .label_delete_unused import command_object
@@ -0,0 +1,82 @@
1
+ import argparse
2
+ from typing import Optional, List
3
+
4
+ import illumio_pylo as pylo
5
+ import json
6
+ import hashlib
7
+
8
+ from illumio_pylo import log
9
+ from . import Command
10
+ from illumio_pylo.API.JsonPayloadTypes import LabelObjectJsonStructure
11
+
12
+ command_name = "label-delete-unused"
13
+ objects_load_filter = [] # No need to load any objects from PCE
14
+
15
+
16
+ def fill_parser(parser: argparse.ArgumentParser):
17
+ parser.add_argument('--confirm', action='store_true',
18
+ help='No change will be implemented in the PCE until you use this function to confirm you\'re good with them after review')
19
+ parser.add_argument('--limit', type=int, required=False, default=None,
20
+ help='Maximum number of unused labels to delete (default: all found unused labels)')
21
+
22
+
23
+ def __main(args, org: pylo.Organization = None, connector: pylo.APIConnector = None, config_data=None, **kwargs):
24
+
25
+ settings_confirmed_changes: bool = args['confirm']
26
+ settings_limit_deletions: Optional[int] = args['limit']
27
+
28
+ print("Fetching all Labels from the PCE... ", end='', flush=True)
29
+ # pylo.log_set_debug()
30
+ labels_json = connector.objects_label_get(max_results=199000, get_usage=True, async_mode=False)
31
+ print("OK!")
32
+
33
+ print(f"Analyzing {len(labels_json)} labels to find unused ones... ")
34
+ unused_labels: List[LabelObjectJsonStructure] = []
35
+
36
+ for label_json in labels_json:
37
+ usage_json = label_json.get('usage', {})
38
+ label_is_used = False
39
+
40
+ for usage_type, usage_confirmed in usage_json.items():
41
+ if usage_confirmed:
42
+ label_is_used = True
43
+ print(f"Label '{label_json.get('value')}' is used in '{usage_type}', skipping deletion.")
44
+ break
45
+
46
+ if not label_is_used:
47
+ print(f"Label '{label_json.get('value')}' is unused, marking for deletion.")
48
+ unused_labels.append(label_json)
49
+
50
+ print()
51
+ print(f"Found {len(unused_labels)} unused labels vs total of {len(labels_json)} labels.")
52
+
53
+ if len(unused_labels) > 0:
54
+ if not settings_confirmed_changes:
55
+ print("No change will be implemented in the PCE until you use the '--confirm' flag to confirm you're good with them after review.")
56
+ else:
57
+ print()
58
+ print(f"Proceeding to delete unused labels up to the limit of '{settings_limit_deletions if settings_limit_deletions is not None else 'all'}'...")
59
+ tracker = connector.new_tracker_for_label_multi_deletion()
60
+
61
+ if settings_limit_deletions is not None:
62
+ unused_labels = unused_labels[:settings_limit_deletions]
63
+
64
+ for label_json in unused_labels:
65
+ tracker.add_label(label_json['href'])
66
+
67
+ tracker.execute_deletion()
68
+ errors_count = tracker.get_errors_count()
69
+ success_count = len(unused_labels) - errors_count
70
+
71
+ for label_json in unused_labels:
72
+ error = tracker.get_error(label_json['href'])
73
+ if error is not None:
74
+ print(f" - ERROR deleting label '{label_json.get('value')}': {error}")
75
+ else:
76
+ print(f" - SUCCESS deleting label '{label_json.get('value')}'")
77
+
78
+ print()
79
+ print(f"Deletion completed: {success_count} labels deleted successfully, {errors_count} errors encountered.")
80
+
81
+
82
+ command_object = Command(command_name, __main, fill_parser, skip_pce_config_loading=True, load_specific_objects_only=objects_load_filter)
@@ -4,7 +4,7 @@ import click
4
4
  import argparse
5
5
 
6
6
  import illumio_pylo as pylo
7
- from illumio_pylo import ExcelHeader
7
+ from illumio_pylo import ExcelHeader, nice_json
8
8
 
9
9
  from .utils.misc import make_filename_with_timestamp
10
10
  from . import Command
@@ -134,8 +134,8 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
134
134
  url_link_to_pce = workload.get_pce_ui_url()
135
135
  new_row = {
136
136
  'hostname': workload.hostname,
137
- 'online': workload.online,
138
- 'last_heartbeat': workload.ven_agent.get_last_heartbeat_date().strftime('%Y-%m-%d %H:%M'),
137
+ 'online': workload.online if not workload.unmanaged else 'UNMANAGED',
138
+ 'last_heartbeat': workload.ven_agent.get_last_heartbeat_date().strftime('%Y-%m-%d %H:%M') if not workload.unmanaged else 'UNMANAGED',
139
139
  'created_at': workload.created_at_datetime().strftime('%Y-%m-%d %H:%M'),
140
140
  'href': workload.href,
141
141
  'link_to_pce': url_link_to_pce,
@@ -172,48 +172,66 @@ def __main(args, org: pylo.Organization, pce_cache_was_used: bool, **kwargs):
172
172
  len(dup_record.offline),
173
173
  len(dup_record.unmanaged)))
174
174
 
175
- latest_created_workload = dup_record.find_latest_created_at()
176
- latest_heartbeat_workload = dup_record.find_latest_heartbeat()
177
- print(" - Latest created at {} and latest heartbeat at {}".format(latest_created_workload.created_at, latest_heartbeat_workload.ven_agent.get_last_heartbeat_date()))
175
+ if not dup_record.has_no_managed_workloads():
176
+ latest_created_workload = dup_record.find_latest_managed_created_at()
177
+ latest_heartbeat_workload = dup_record.find_latest_heartbeat()
178
178
 
179
- if dup_record.count_online() == 0:
180
- print(" - IGNORED: there is no VEN online")
181
- for wkl in dup_record.offline:
182
- add_workload_to_report(wkl, "ignored (no VEN online)")
183
- continue
179
+ print(" - Latest created at {} and latest heartbeat at {}".format(latest_created_workload.created_at, latest_heartbeat_workload.ven_agent.get_last_heartbeat_date()))
184
180
 
185
- if dup_record.count_online() > 1:
186
- print(" - WARNING: there are more than 1 VEN online")
187
-
188
- # Don't delete online workloads but still show them in the report
189
- for wkl in dup_record.online:
190
- add_workload_to_report(wkl, "ignored (VEN is online)")
191
-
192
- for wkl in dup_record.offline:
193
- if arg_do_not_delete_the_most_recent_workload and wkl is latest_created_workload:
194
- print(" - IGNORED: wkl {}/{} is the most recent".format(wkl.get_name_stripped_fqdn(), wkl.href))
195
- add_workload_to_report(wkl, "ignored (it is the most recently created)")
196
- elif arg_do_not_delete_the_most_recently_heartbeating_workload and wkl is latest_heartbeat_workload:
197
- print(" - IGNORED: wkl {}/{} is the most recently heartbeating".format(wkl.get_name_stripped_fqdn(), wkl.href))
198
- add_workload_to_report(wkl, "ignored (it is the most recently heartbeating)")
199
- elif arg_do_not_delete_if_last_heartbeat_is_more_recent_than is not None and wkl.ven_agent.get_last_heartbeat_date() > datetime.datetime.now() - datetime.timedelta(days=arg_do_not_delete_if_last_heartbeat_is_more_recent_than):
200
- print(" - IGNORED: wkl {}/{} has a last heartbeat more recent than {} days".format(wkl.get_name_stripped_fqdn(), wkl.href, arg_do_not_delete_if_last_heartbeat_is_more_recent_than))
201
- add_workload_to_report(wkl, "ignored (last heartbeat is more recent than {} days)".format(arg_do_not_delete_if_last_heartbeat_is_more_recent_than))
202
- else:
181
+ if dup_record.count_online() == 0:
182
+ print(" - IGNORED: there is no VEN online")
183
+ for wkl in dup_record.offline:
184
+ add_workload_to_report(wkl, "ignored (no VEN online)")
185
+ continue
186
+
187
+ if dup_record.count_online() > 1:
188
+ print(" - WARNING: there are more than 1 VEN online")
189
+
190
+ # Don't delete online workloads but still show them in the report
191
+ for wkl in dup_record.online:
192
+ add_workload_to_report(wkl, "ignored (VEN is online)")
193
+
194
+ for wkl in dup_record.offline:
195
+ if arg_do_not_delete_the_most_recent_workload and wkl is latest_created_workload:
196
+ print(" - IGNORED: wkl {}/{} is the most recent".format(wkl.get_name_stripped_fqdn(), wkl.href))
197
+ add_workload_to_report(wkl, "ignored (it is the most recently created)")
198
+ elif arg_do_not_delete_the_most_recently_heartbeating_workload and wkl is latest_heartbeat_workload:
199
+ print(" - IGNORED: wkl {}/{} is the most recently heartbeating".format(wkl.get_name_stripped_fqdn(), wkl.href))
200
+ add_workload_to_report(wkl, "ignored (it is the most recently heartbeating)")
201
+ elif arg_do_not_delete_if_last_heartbeat_is_more_recent_than is not None and wkl.ven_agent.get_last_heartbeat_date() > datetime.datetime.now() - datetime.timedelta(days=arg_do_not_delete_if_last_heartbeat_is_more_recent_than):
202
+ print(" - IGNORED: wkl {}/{} has a last heartbeat more recent than {} days".format(wkl.get_name_stripped_fqdn(), wkl.href, arg_do_not_delete_if_last_heartbeat_is_more_recent_than))
203
+ add_workload_to_report(wkl, "ignored (last heartbeat is more recent than {} days)".format(arg_do_not_delete_if_last_heartbeat_is_more_recent_than))
204
+ else:
205
+ if arg_limit_number_of_deleted_workloads is not None and delete_tracker.count_entries() >= arg_limit_number_of_deleted_workloads:
206
+ print(" - IGNORED: wkl {}/{} because the limit of {} workloads to be deleted was reached".format(wkl.get_name_stripped_fqdn(), wkl.href, arg_limit_number_of_deleted_workloads))
207
+ add_workload_to_report(wkl, "ignored (limit of {} workloads to be deleted was reached)".format(arg_limit_number_of_deleted_workloads))
208
+ else:
209
+ delete_tracker.add_workload(wkl)
210
+ print(" - added offline wkl {}/{} to the delete list".format(wkl.get_name_stripped_fqdn(), wkl.href))
211
+
212
+ for wkl in dup_record.unmanaged:
203
213
  if arg_limit_number_of_deleted_workloads is not None and delete_tracker.count_entries() >= arg_limit_number_of_deleted_workloads:
204
214
  print(" - IGNORED: wkl {}/{} because the limit of {} workloads to be deleted was reached".format(wkl.get_name_stripped_fqdn(), wkl.href, arg_limit_number_of_deleted_workloads))
205
215
  add_workload_to_report(wkl, "ignored (limit of {} workloads to be deleted was reached)".format(arg_limit_number_of_deleted_workloads))
206
216
  else:
207
217
  delete_tracker.add_workload(wkl)
208
- print(" - added offline wkl {}/{} to the delete list".format(wkl.get_name_stripped_fqdn(), wkl.href))
209
-
210
- for wkl in dup_record.unmanaged:
211
- if arg_limit_number_of_deleted_workloads is not None and delete_tracker.count_entries() >= arg_limit_number_of_deleted_workloads:
212
- print(" - IGNORED: wkl {}/{} because the limit of {} workloads to be deleted was reached".format(wkl.get_name_stripped_fqdn(), wkl.href, arg_limit_number_of_deleted_workloads))
213
- add_workload_to_report(wkl, "ignored (limit of {} workloads to be deleted was reached)".format(arg_limit_number_of_deleted_workloads))
214
- else:
215
- delete_tracker.add_workload(wkl)
216
- print(" - added unmanaged wkl {}/{} to the delete list".format(wkl.get_name_stripped_fqdn(), wkl.href))
218
+ print(" - added unmanaged wkl {}/{} to the delete list".format(wkl.get_name_stripped_fqdn(), wkl.href))
219
+ else:
220
+ latest_created_workload = dup_record.find_latest_unmanaged_created_at()
221
+ if latest_created_workload is None:
222
+ raise pylo.PyloEx("Internal error: cannot find the latest created unmanaged workload for hostname '{}'".format(dup_hostname))
223
+ print(" - All workloads are unmanaged. Latest created at {} will be kept".format(latest_created_workload.created_at))
224
+ for wkl in dup_record.unmanaged:
225
+ if wkl is latest_created_workload:
226
+ print(" - IGNORED: wkl {}/{} is the most recent".format(wkl.get_name_stripped_fqdn(), wkl.href))
227
+ add_workload_to_report(wkl, "ignored (it is the most recently created)")
228
+ else:
229
+ if arg_limit_number_of_deleted_workloads is not None and delete_tracker.count_entries() >= arg_limit_number_of_deleted_workloads:
230
+ print(" - IGNORED: wkl {}/{} because the limit of {} workloads to be deleted was reached".format(wkl.get_name_stripped_fqdn(), wkl.href, arg_limit_number_of_deleted_workloads))
231
+ add_workload_to_report(wkl, "ignored (limit of {} workloads to be deleted was reached)".format(arg_limit_number_of_deleted_workloads))
232
+ else:
233
+ delete_tracker.add_workload(wkl)
234
+ print(" - added unmanaged wkl {}/{} to the delete list".format(wkl.get_name_stripped_fqdn(), wkl.href))
217
235
 
218
236
  print()
219
237
 
@@ -324,7 +342,12 @@ class DuplicateRecordManager:
324
342
  return True
325
343
  return False
326
344
 
327
- def find_latest_created_at(self) -> 'pylo.Workload':
345
+ def has_no_managed_workloads(self):
346
+ if len(self.offline) + len(self.online) == 0:
347
+ return True
348
+ return False
349
+
350
+ def find_latest_managed_created_at(self) -> Optional['pylo.Workload']:
328
351
  latest: Optional[pylo.Workload] = None
329
352
  for wkl in self.all:
330
353
  if wkl.unmanaged:
@@ -333,7 +356,16 @@ class DuplicateRecordManager:
333
356
  latest = wkl
334
357
  return latest
335
358
 
336
- def find_latest_heartbeat(self) -> 'pylo.Workload':
359
+ def find_latest_unmanaged_created_at(self) -> Optional['pylo.Workload']:
360
+ latest: Optional[pylo.Workload] = None
361
+ for wkl in self.all:
362
+ if not wkl.unmanaged:
363
+ continue
364
+ if latest is None or wkl.created_at > latest.created_at:
365
+ latest = wkl
366
+ return latest
367
+
368
+ def find_latest_heartbeat(self) -> Optional['pylo.Workload']:
337
369
  latest: Optional[pylo.Workload] = None
338
370
  for wkl in self.all:
339
371
  if wkl.unmanaged:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: illumio_pylo
3
- Version: 0.3.9
3
+ Version: 0.3.11
4
4
  Summary: A set of tools and library for working with Illumio PCE
5
5
  Home-page: https://github.com/cpainchaud/pylo
6
6
  Author: Christophe Painchaud
@@ -187,11 +187,17 @@ Requires-Python: >=3.11
187
187
  License-File: LICENSE
188
188
  Requires-Dist: click==8.1.7
189
189
  Requires-Dist: colorama~=0.4.4
190
- Requires-Dist: cryptography==42.0.7
190
+ Requires-Dist: cryptography==44.0.1
191
191
  Requires-Dist: openpyxl~=3.1.3
192
192
  Requires-Dist: paramiko~=3.4.0
193
193
  Requires-Dist: prettytable~=3.10.0
194
194
  Requires-Dist: requests~=2.32.0
195
195
  Requires-Dist: xlsxwriter~=3.2.0
196
+ Dynamic: author
197
+ Dynamic: author-email
198
+ Dynamic: description
199
+ Dynamic: home-page
200
+ Dynamic: license-file
201
+ Dynamic: requires-python
196
202
 
197
203
  README.md
@@ -82,6 +82,7 @@ illumio_pylo/cli/commands/__init__.py
82
82
  illumio_pylo/cli/commands/credential_manager.py
83
83
  illumio_pylo/cli/commands/iplist_analyzer.py
84
84
  illumio_pylo/cli/commands/iplist_import_from_file.py
85
+ illumio_pylo/cli/commands/label_delete_unused.py
85
86
  illumio_pylo/cli/commands/ruleset_export.py
86
87
  illumio_pylo/cli/commands/update_pce_objects_cache.py
87
88
  illumio_pylo/cli/commands/ven_compatibility_report_export.py
@@ -1,6 +1,6 @@
1
1
  click==8.1.7
2
2
  colorama~=0.4.4
3
- cryptography==42.0.7
3
+ cryptography==44.0.1
4
4
  openpyxl~=3.1.3
5
5
  paramiko~=3.4.0
6
6
  prettytable~=3.10.0
@@ -1,6 +1,6 @@
1
1
  click==8.1.7
2
2
  colorama~=0.4.4
3
- cryptography==42.0.7
3
+ cryptography==44.0.1
4
4
  openpyxl~=3.1.3
5
5
  paramiko~=3.4.0
6
6
  prettytable~=3.10.0
File without changes
File without changes
File without changes
File without changes
File without changes