PyADRecon 0.13.2__tar.gz → 0.13.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 (27) hide show
  1. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/CHANGELOG.md +7 -7
  2. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/PKG-INFO +1 -1
  3. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/PyADRecon.egg-info/PKG-INFO +1 -1
  4. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/dashboard_generator.py +690 -15
  5. pyadrecon-0.13.3/package.json +3 -0
  6. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/pyadrecon.py +1 -1
  7. pyadrecon-0.13.2/package.json +0 -3
  8. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/.dockerignore +0 -0
  9. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/.github/FUNDING.yml +0 -0
  10. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/.github/pyadrecon.png +0 -0
  13. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/.github/workflows/conventional-commits.yml +0 -0
  14. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/.github/workflows/pypi.yml +0 -0
  15. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/.gitignore +0 -0
  16. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/Dockerfile +0 -0
  17. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/LICENSE +0 -0
  18. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/PyADRecon.egg-info/SOURCES.txt +0 -0
  19. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/PyADRecon.egg-info/dependency_links.txt +0 -0
  20. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/PyADRecon.egg-info/entry_points.txt +0 -0
  21. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/PyADRecon.egg-info/requires.txt +0 -0
  22. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/PyADRecon.egg-info/top_level.txt +0 -0
  23. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/README.md +0 -0
  24. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/pyproject.toml +0 -0
  25. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/requirements.txt +0 -0
  26. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/setup.cfg +0 -0
  27. {pyadrecon-0.13.2 → pyadrecon-0.13.3}/windows/README.md +0 -0
@@ -1,3 +1,10 @@
1
+ ## [0.13.3](https://github.com/l4rm4nd/PyADRecon/compare/v0.13.2...v0.13.3) (2026-02-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add EOL systemd detection ([81bdb7f](https://github.com/l4rm4nd/PyADRecon/commit/81bdb7f516a3023af08b7cab2be3b6242d68c174))
7
+
1
8
  ## [0.13.2](https://github.com/l4rm4nd/PyADRecon/compare/v0.13.1...v0.13.2) (2026-02-22)
2
9
 
3
10
 
@@ -26,10 +33,3 @@
26
33
 
27
34
  * rename finding ([453536a](https://github.com/l4rm4nd/PyADRecon/commit/453536a5bf760db7ae28a759c5e11a77ee1adb1e))
28
35
 
29
- ## [0.12.13](https://github.com/l4rm4nd/PyADRecon/compare/v0.12.12...v0.12.13) (2026-02-22)
30
-
31
-
32
- ### Bug Fixes
33
-
34
- * rework protected users in dashboard ([b07d6e6](https://github.com/l4rm4nd/PyADRecon/commit/b07d6e6e88c3ad7503a41e2fbfd83231ee9016be))
35
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyADRecon
3
- Version: 0.13.2
3
+ Version: 0.13.3
4
4
  Summary: Python Active Directory Reconnaissance Tool (ADRecon port) with NTLM and Kerberos support.
5
5
  Author: LRVT
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyADRecon
3
- Version: 0.13.2
3
+ Version: 0.13.3
4
4
  Summary: Python Active Directory Reconnaissance Tool (ADRecon port) with NTLM and Kerberos support.
5
5
  Author: LRVT
6
6
  License-Expression: MIT
@@ -8,7 +8,9 @@ import os
8
8
  import csv
9
9
  import json
10
10
  import base64
11
- from datetime import datetime
11
+ import urllib.request
12
+ import urllib.error
13
+ from datetime import datetime, timedelta
12
14
  from pathlib import Path
13
15
 
14
16
 
@@ -634,6 +636,23 @@ class DashboardGenerator:
634
636
  </div>
635
637
  </div>
636
638
  </div>
639
+
640
+ <div v-if="legacyOperatingSystems.length > 0" @click="navigateToSection('findings', 'legacy-os-section')" class="stat-card bg-white dark:bg-gray-800 rounded-lg shadow p-6 cursor-pointer hover:shadow-lg transition-shadow">
641
+ <div class="flex items-center">
642
+ <div class="flex-shrink-0 bg-red-100 dark:bg-red-900/30 rounded-md p-3">
643
+ <i class="fas fa-desktop text-red-600 text-2xl"></i>
644
+ </div>
645
+ <div class="ml-5 w-0 flex-1">
646
+ <dl>
647
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Legacy OS Systems</dt>
648
+ <dd class="text-3xl font-semibold text-gray-900 dark:text-white">{{{{ legacyOperatingSystems.length }}}}</dd>
649
+ <dd class="text-xs text-gray-500 dark:text-gray-400 mt-1">
650
+ Unpatched or End-of-Life (EOL)
651
+ </dd>
652
+ </dl>
653
+ </div>
654
+ </div>
655
+ </div>
637
656
  </div>
638
657
  </div>
639
658
 
@@ -1746,7 +1765,7 @@ class DashboardGenerator:
1746
1765
  </tr>
1747
1766
  </thead>
1748
1767
  <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
1749
- <tr v-for="account in unconstrainedDelegationAccounts" :key="account.DN">
1768
+ <tr v-for="account in paginatedUnconstrainedDelegation" :key="account.DN">
1750
1769
  <td class="px-6 py-4">
1751
1770
  <span v-if="account.Type === 'User'" class="badge badge-info">User</span>
1752
1771
  <span v-else class="badge badge-purple">Computer</span>
@@ -1784,9 +1803,24 @@ class DashboardGenerator:
1784
1803
  </tbody>
1785
1804
  </table>
1786
1805
  </div>
1806
+ <div v-if="unconstrainedDelegationAccounts.length > itemsPerPage" class="flex justify-between items-center py-4">
1807
+ <div class="text-sm text-gray-500 dark:text-gray-400">
1808
+ Showing {{{{ ((unconstrainedDelegationPage - 1) * itemsPerPage) + 1 }}}} to {{{{ Math.min(unconstrainedDelegationPage * itemsPerPage, unconstrainedDelegationAccounts.length) }}}} of {{{{ unconstrainedDelegationAccounts.length }}}} accounts
1809
+ </div>
1810
+ <div class="flex gap-2">
1811
+ <button @click="unconstrainedDelegationPage = Math.max(1, unconstrainedDelegationPage - 1)" :disabled="unconstrainedDelegationPage === 1"
1812
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
1813
+ <i class="fas fa-chevron-left"></i> Previous
1814
+ </button>
1815
+ <span class="px-3 py-1">Page {{{{ unconstrainedDelegationPage }}}} of {{{{ Math.ceil(unconstrainedDelegationAccounts.length / itemsPerPage) }}}}</span>
1816
+ <button @click="unconstrainedDelegationPage = Math.min(Math.ceil(unconstrainedDelegationAccounts.length / itemsPerPage), unconstrainedDelegationPage + 1)"
1817
+ :disabled="unconstrainedDelegationPage >= Math.ceil(unconstrainedDelegationAccounts.length / itemsPerPage)"
1818
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
1819
+ Next <i class="fas fa-chevron-right"></i>
1820
+ </button>
1821
+ </div>
1822
+ </div>
1787
1823
  </div>
1788
-
1789
- <!-- Constrained Delegation Section -->
1790
1824
  <div id="constrained-delegation-section" v-if="constrainedDelegationRisks.length > 0" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
1791
1825
  <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
1792
1826
  <i class="fas fa-exchange-alt text-orange-600"></i> Constrained Delegation Accounts ({{{{ constrainedDelegationRisks.length }}}})
@@ -1843,7 +1877,7 @@ class DashboardGenerator:
1843
1877
  </tr>
1844
1878
  </thead>
1845
1879
  <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
1846
- <tr v-for="account in constrainedDelegationRisks" :key="account.DN">
1880
+ <tr v-for="account in paginatedConstrainedDelegation" :key="account.DN">
1847
1881
  <td class="px-6 py-4">
1848
1882
  <span v-if="account.Type === 'User'" class="badge badge-info">User</span>
1849
1883
  <span v-else class="badge badge-purple">Computer</span>
@@ -1885,9 +1919,24 @@ class DashboardGenerator:
1885
1919
  </tbody>
1886
1920
  </table>
1887
1921
  </div>
1922
+ <div v-if="constrainedDelegationRisks.length > itemsPerPage" class="flex justify-between items-center py-4">
1923
+ <div class="text-sm text-gray-500 dark:text-gray-400">
1924
+ Showing {{{{ ((constrainedDelegationPage - 1) * itemsPerPage) + 1 }}}} to {{{{ Math.min(constrainedDelegationPage * itemsPerPage, constrainedDelegationRisks.length) }}}} of {{{{ constrainedDelegationRisks.length }}}} accounts
1925
+ </div>
1926
+ <div class="flex gap-2">
1927
+ <button @click="constrainedDelegationPage = Math.max(1, constrainedDelegationPage - 1)" :disabled="constrainedDelegationPage === 1"
1928
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
1929
+ <i class="fas fa-chevron-left"></i> Previous
1930
+ </button>
1931
+ <span class="px-3 py-1">Page {{{{ constrainedDelegationPage }}}} of {{{{ Math.ceil(constrainedDelegationRisks.length / itemsPerPage) }}}}</span>
1932
+ <button @click="constrainedDelegationPage = Math.min(Math.ceil(constrainedDelegationRisks.length / itemsPerPage), constrainedDelegationPage + 1)"
1933
+ :disabled="constrainedDelegationPage >= Math.ceil(constrainedDelegationRisks.length / itemsPerPage)"
1934
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
1935
+ Next <i class="fas fa-chevron-right"></i>
1936
+ </button>
1937
+ </div>
1938
+ </div>
1888
1939
  </div>
1889
-
1890
- <!-- SID History Section -->
1891
1940
  <div id="sidhistory-section" v-if="objectsWithSIDHistory.length > 0" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
1892
1941
  <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
1893
1942
  <i class="fas fa-history text-purple-600"></i> Objects with SID History ({{{{ objectsWithSIDHistory.length }}}})
@@ -1935,7 +1984,7 @@ class DashboardGenerator:
1935
1984
  </tr>
1936
1985
  </thead>
1937
1986
  <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
1938
- <tr v-for="obj in objectsWithSIDHistory" :key="obj.DN">
1987
+ <tr v-for="obj in paginatedSIDHistory" :key="obj.DN">
1939
1988
  <td class="px-6 py-4">
1940
1989
  <span v-if="obj.Type === 'User'" class="badge badge-info">User</span>
1941
1990
  <span v-else-if="obj.Type === 'Computer'" class="badge badge-purple">Computer</span>
@@ -1963,9 +2012,24 @@ class DashboardGenerator:
1963
2012
  </tbody>
1964
2013
  </table>
1965
2014
  </div>
2015
+ <div v-if="objectsWithSIDHistory.length > itemsPerPage" class="flex justify-between items-center py-4">
2016
+ <div class="text-sm text-gray-500 dark:text-gray-400">
2017
+ Showing {{{{ ((sidHistoryPage - 1) * itemsPerPage) + 1 }}}} to {{{{ Math.min(sidHistoryPage * itemsPerPage, objectsWithSIDHistory.length) }}}} of {{{{ objectsWithSIDHistory.length }}}} objects
2018
+ </div>
2019
+ <div class="flex gap-2">
2020
+ <button @click="sidHistoryPage = Math.max(1, sidHistoryPage - 1)" :disabled="sidHistoryPage === 1"
2021
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
2022
+ <i class="fas fa-chevron-left"></i> Previous
2023
+ </button>
2024
+ <span class="px-3 py-1">Page {{{{ sidHistoryPage }}}} of {{{{ Math.ceil(objectsWithSIDHistory.length / itemsPerPage) }}}}</span>
2025
+ <button @click="sidHistoryPage = Math.min(Math.ceil(objectsWithSIDHistory.length / itemsPerPage), sidHistoryPage + 1)"
2026
+ :disabled="sidHistoryPage >= Math.ceil(objectsWithSIDHistory.length / itemsPerPage)"
2027
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
2028
+ Next <i class="fas fa-chevron-right"></i>
2029
+ </button>
2030
+ </div>
2031
+ </div>
1966
2032
  </div>
1967
-
1968
- <!-- Foreign Security Principals Section -->
1969
2033
  <div id="foreign-principals-section" v-if="foreignSecurityPrincipals.length > 0" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
1970
2034
  <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
1971
2035
  <i class="fas fa-globe text-pink-600"></i> Foreign Security Principals in Privileged Groups ({{{{ foreignSecurityPrincipals.length }}}})
@@ -2010,7 +2074,7 @@ class DashboardGenerator:
2010
2074
  </tr>
2011
2075
  </thead>
2012
2076
  <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
2013
- <tr v-for="fsp in foreignSecurityPrincipals" :key="fsp['Member SID']">
2077
+ <tr v-for="fsp in paginatedForeignPrincipals" :key="fsp['Member SID']">
2014
2078
  <td class="px-6 py-4 font-medium">{{{{ fsp['Group Name'] }}}}</td>
2015
2079
  <td class="px-6 py-4">{{{{ fsp['Member Name'] }}}}</td>
2016
2080
  <td class="px-6 py-4 text-xs font-mono max-w-xs truncate" :title="fsp['Member SID']">
@@ -2029,9 +2093,24 @@ class DashboardGenerator:
2029
2093
  </tbody>
2030
2094
  </table>
2031
2095
  </div>
2096
+ <div v-if="foreignSecurityPrincipals.length > itemsPerPage" class="flex justify-between items-center py-4">
2097
+ <div class="text-sm text-gray-500 dark:text-gray-400">
2098
+ Showing {{{{ ((foreignPrincipalsPage - 1) * itemsPerPage) + 1 }}}} to {{{{ Math.min(foreignPrincipalsPage * itemsPerPage, foreignSecurityPrincipals.length) }}}} of {{{{ foreignSecurityPrincipals.length }}}} principals
2099
+ </div>
2100
+ <div class="flex gap-2">
2101
+ <button @click="foreignPrincipalsPage = Math.max(1, foreignPrincipalsPage - 1)" :disabled="foreignPrincipalsPage === 1"
2102
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
2103
+ <i class="fas fa-chevron-left"></i> Previous
2104
+ </button>
2105
+ <span class="px-3 py-1">Page {{{{ foreignPrincipalsPage }}}} of {{{{ Math.ceil(foreignSecurityPrincipals.length / itemsPerPage) }}}}</span>
2106
+ <button @click="foreignPrincipalsPage = Math.min(Math.ceil(foreignSecurityPrincipals.length / itemsPerPage), foreignPrincipalsPage + 1)"
2107
+ :disabled="foreignPrincipalsPage >= Math.ceil(foreignSecurityPrincipals.length / itemsPerPage)"
2108
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
2109
+ Next <i class="fas fa-chevron-right"></i>
2110
+ </button>
2111
+ </div>
2112
+ </div>
2032
2113
  </div>
2033
-
2034
- <!-- gMSA Vulnerabilities Section -->
2035
2114
  <div id="gmsa-vulnerabilities-section" v-if="vulnerableGMSA.length > 0" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
2036
2115
  <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
2037
2116
  <i class="fas fa-user-cog text-yellow-600"></i> gMSA Misconfigurations ({{{{ vulnerableGMSA.length }}}})
@@ -2076,7 +2155,7 @@ class DashboardGenerator:
2076
2155
  </tr>
2077
2156
  </thead>
2078
2157
  <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
2079
- <tr v-for="gmsa in vulnerableGMSA" :key="gmsa.DN">
2158
+ <tr v-for="gmsa in paginatedGMSAVuln" :key="gmsa.DN">
2080
2159
  <td class="px-6 py-4 font-medium">{{{{ gmsa.Name }}}}</td>
2081
2160
  <td class="px-6 py-4 text-sm max-w-xs truncate" :title="gmsa['Allowed To Retrieve']">
2082
2161
  {{{{ gmsa['Allowed To Retrieve'] || 'N/A' }}}}
@@ -2096,6 +2175,131 @@ class DashboardGenerator:
2096
2175
  </tbody>
2097
2176
  </table>
2098
2177
  </div>
2178
+ <div v-if="vulnerableGMSA.length > itemsPerPage" class="flex justify-between items-center py-4">
2179
+ <div class="text-sm text-gray-500 dark:text-gray-400">
2180
+ Showing {{{{ ((gmsaVulnPage - 1) * itemsPerPage) + 1 }}}} to {{{{ Math.min(gmsaVulnPage * itemsPerPage, vulnerableGMSA.length) }}}} of {{{{ vulnerableGMSA.length }}}} gMSAs
2181
+ </div>
2182
+ <div class="flex gap-2">
2183
+ <button @click="gmsaVulnPage = Math.max(1, gmsaVulnPage - 1)" :disabled="gmsaVulnPage === 1"
2184
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
2185
+ <i class="fas fa-chevron-left"></i> Previous
2186
+ </button>
2187
+ <span class="px-3 py-1">Page {{{{ gmsaVulnPage }}}} of {{{{ Math.ceil(vulnerableGMSA.length / itemsPerPage) }}}}</span>
2188
+ <button @click="gmsaVulnPage = Math.min(Math.ceil(vulnerableGMSA.length / itemsPerPage), gmsaVulnPage + 1)"
2189
+ :disabled="gmsaVulnPage >= Math.ceil(vulnerableGMSA.length / itemsPerPage)"
2190
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
2191
+ Next <i class="fas fa-chevron-right"></i>
2192
+ </button>
2193
+ </div>
2194
+ </div>
2195
+ </div>
2196
+ <div id="legacy-os-section" v-if="legacyOperatingSystems.length > 0" class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
2197
+ <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
2198
+ <i class="fas fa-desktop text-red-600"></i> Legacy Operating Systems ({{{{ legacyOperatingSystems.length }}}})
2199
+ </h2>
2200
+ <div class="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500 p-4 mb-6">
2201
+ <h3 class="font-semibold text-red-800 dark:text-red-200 mb-2">🎯 Issue & Impact</h3>
2202
+ <p class="text-red-700 dark:text-red-300 text-sm mb-3">
2203
+ End-of-life (EOL) operating systems no longer receive security updates from Microsoft, leaving known vulnerabilities unpatched indefinitely. Systems flagged here are either already EOL or will reach EOL within 12 months. These systems are prime targets for attackers using publicly available exploits. Legacy systems lack modern security features like Credential Guard, Windows Defender Application Control, and enhanced SMB security.
2204
+ </p>
2205
+ <h3 class="font-semibold text-red-800 dark:text-red-200 mb-2">⚔️ Attacker Benefit</h3>
2206
+ <p class="text-red-700 dark:text-red-300 text-sm mb-3">
2207
+ EOL systems provide easy entry points with known CVEs and no patches available. Legacy Windows systems lack modern credential protection - NTLM hashes stored in memory are trivial to extract with Mimikatz. These systems can't enforce modern Kerberos protections or use Virtual Secure Mode. Attackers use these as pivot points to compromise the domain, knowing the vulnerabilities won't be patched.
2208
+ </p>
2209
+ <h3 class="font-semibold text-red-800 dark:text-red-200 mb-2">🔓 Exploitation Method</h3>
2210
+ <p class="text-red-700 dark:text-red-300 text-sm mb-3">
2211
+ Tools: Metasploit (EternalBlue/MS17-010), Mimikatz, Responder, PrintNightmare exploits. Attackers scan for legacy OS versions, exploit unpatched vulnerabilities (SMBv1, Print Spooler, legacy authentication), extract credentials from memory, move laterally using pass-the-hash. Common exploits: EternalBlue (Server 2008), BlueKeep (Windows 7), ZeroLogon (Server 2008/2012), PrintNightmare (all legacy versions). Systems soon reaching EOL face increasing risk as vendors reduce patch frequency.
2212
+ </p>
2213
+ <h3 class="font-semibold text-red-800 dark:text-red-200 mb-2">💡 Remediation</h3>
2214
+ <p class="text-red-700 dark:text-red-300 text-sm">
2215
+ <strong>Priority-based migration:</strong> CRITICAL (security support ended) - immediate migration required. INFO (EOL within 12 months) - begin migration planning now. Upgrade to Windows 10/11 for workstations and Server 2019/2022/2025 for servers. If immediate migration is not possible: Isolate legacy systems via network segmentation, implement strict firewall rules, disable unnecessary services (especially SMBv1), restrict access to privileged accounts only, monitor aggressively for exploitation attempts. Document business justification for any systems that cannot be upgraded. Note: Systems with 12+ months of security support remaining are not shown here.
2216
+ </p>
2217
+ </div>
2218
+ <div class="overflow-x-auto">
2219
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
2220
+ <thead class="bg-gray-50 dark:bg-gray-700">
2221
+ <tr>
2222
+ <th @click="sortLegacyOS('Name')" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
2223
+ Computer Name <i v-if="legacyOsSortColumn === 'Name'" :class="legacyOsSortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'" class="ml-1"></i>
2224
+ </th>
2225
+ <th @click="sortLegacyOS('Operating System')" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
2226
+ Operating System <i v-if="legacyOsSortColumn === 'Operating System'" :class="legacyOsSortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'" class="ml-1"></i>
2227
+ </th>
2228
+ <th @click="sortLegacyOS('EOL Date')" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
2229
+ EOL Date <i v-if="legacyOsSortColumn === 'EOL Date'" :class="legacyOsSortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'" class="ml-1"></i>
2230
+ </th>
2231
+ <th @click="sortLegacyOS('Days Since EOL')" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
2232
+ Days Until/Since EOL <i v-if="legacyOsSortColumn === 'Days Since EOL'" :class="legacyOsSortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'" class="ml-1"></i>
2233
+ </th>
2234
+ <th @click="sortLegacyOS('EOL Status')" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
2235
+ Status <i v-if="legacyOsSortColumn === 'EOL Status'" :class="legacyOsSortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'" class="ml-1"></i>
2236
+ </th>
2237
+ <th @click="sortLegacyOS('Logon Age (days)')" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
2238
+ Last Logon <i v-if="legacyOsSortColumn === 'Logon Age (days)'" :class="legacyOsSortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'" class="ml-1"></i>
2239
+ </th>
2240
+ <th @click="sortLegacyOS('Risk Level')" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
2241
+ Risk <i v-if="legacyOsSortColumn === 'Risk Level'" :class="legacyOsSortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'" class="ml-1"></i>
2242
+ </th>
2243
+ </tr>
2244
+ </thead>
2245
+ <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
2246
+ <tr v-for="system in paginatedLegacyOS" :key="system.DN">
2247
+ <td class="px-6 py-4">
2248
+ <div class="font-medium">{{{{ system.Name }}}}</div>
2249
+ <div v-if="system.IPv4Address" class="text-xs text-gray-500 dark:text-gray-400 font-mono">{{{{ system.IPv4Address }}}}</div>
2250
+ </td>
2251
+ <td class="px-6 py-4 text-sm">
2252
+ <div>{{{{ system['Operating System'] }}}}</div>
2253
+ <div class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{{{ system['OS Version'] }}}}</div>
2254
+ </td>
2255
+ <td class="px-6 py-4 text-sm">{{{{ system['EOL Date'] }}}}</td>
2256
+ <td class="px-6 py-4">
2257
+ <span v-if="system['Days Since EOL'] < 0" class="text-sm text-orange-600 dark:text-orange-400 font-semibold">
2258
+ {{{{ Math.abs(system['Days Since EOL']) }}}} days until EOL
2259
+ </span>
2260
+ <span v-else class="text-sm text-red-600 dark:text-red-400 font-semibold">
2261
+ {{{{ system['Days Since EOL'] }}}} days since EOL
2262
+ </span>
2263
+ </td>
2264
+ <td class="px-6 py-4">
2265
+ <span v-if="system['EOL Status'] === 'End-of-Life'" class="badge badge-critical">End-of-Life</span>
2266
+ <span v-else-if="system['EOL Status'] === 'SOON EOL'" class="badge badge-info">SOON EOL</span>
2267
+ <span v-else class="badge badge-medium">{{{{ system['EOL Status'] }}}}</span>
2268
+ </td>
2269
+ <td class="px-6 py-4 text-sm">
2270
+ <span v-if="system['Logon Age (days)']" :class="{{'text-red-600 dark:text-red-400 font-semibold': parseInt(system['Logon Age (days)']) < 30}}">
2271
+ {{{{ system['Logon Age (days)'] }}}} days ago
2272
+ </span>
2273
+ <span v-else class="text-gray-500 dark:text-gray-400">Never</span>
2274
+ </td>
2275
+ <td class="px-6 py-4">
2276
+ <span v-if="system['Risk Level'] === 'CRITICAL'" class="badge badge-critical">CRITICAL</span>
2277
+ <span v-else-if="system['Risk Level'] === 'HIGH'" class="badge badge-high">HIGH</span>
2278
+ <span v-else-if="system['Risk Level'] === 'MEDIUM'" class="badge badge-medium">MEDIUM</span>
2279
+ <span v-else-if="system['Risk Level'] === 'INFO'" class="badge badge-info">INFO</span>
2280
+ <span v-else class="badge badge-low">LOW</span>
2281
+ </td>
2282
+ </tr>
2283
+ </tbody>
2284
+ </table>
2285
+ </div>
2286
+ <div v-if="legacyOperatingSystems.length > itemsPerPage" class="flex justify-between items-center py-4">
2287
+ <div class="text-sm text-gray-500 dark:text-gray-400">
2288
+ Showing {{{{ ((legacyOsPage - 1) * itemsPerPage) + 1 }}}} to {{{{ Math.min(legacyOsPage * itemsPerPage, legacyOperatingSystems.length) }}}} of {{{{ legacyOperatingSystems.length }}}} systems
2289
+ </div>
2290
+ <div class="flex gap-2">
2291
+ <button @click="legacyOsPage = Math.max(1, legacyOsPage - 1)" :disabled="legacyOsPage === 1"
2292
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
2293
+ <i class="fas fa-chevron-left"></i> Previous
2294
+ </button>
2295
+ <span class="px-3 py-1">Page {{{{ legacyOsPage }}}} of {{{{ Math.ceil(legacyOperatingSystems.length / itemsPerPage) }}}}</span>
2296
+ <button @click="legacyOsPage = Math.min(Math.ceil(legacyOperatingSystems.length / itemsPerPage), legacyOsPage + 1)"
2297
+ :disabled="legacyOsPage >= Math.ceil(legacyOperatingSystems.length / itemsPerPage)"
2298
+ class="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400">
2299
+ Next <i class="fas fa-chevron-right"></i>
2300
+ </button>
2301
+ </div>
2302
+ </div>
2099
2303
  </div>
2100
2304
 
2101
2305
  <!-- Password Policy Analysis -->
@@ -2565,7 +2769,7 @@ class DashboardGenerator:
2565
2769
  lapsPage: 1,
2566
2770
  vulnCertTemplatePage: 1,
2567
2771
  cleartextPage: 1,
2568
- itemsPerPage: 50,
2772
+ itemsPerPage: 10,
2569
2773
  certTemplateFilter: 'all',
2570
2774
  certTemplateRiskFilter: 'risk-gt-none',
2571
2775
  userSearch: '',
@@ -2601,6 +2805,14 @@ class DashboardGenerator:
2601
2805
  foreignPrincipalsSortDirection: 'asc',
2602
2806
  gmsaVulnSortColumn: null,
2603
2807
  gmsaVulnSortDirection: 'asc',
2808
+ legacyOsSortColumn: null,
2809
+ legacyOsSortDirection: 'asc',
2810
+ legacyOsPage: 1,
2811
+ unconstrainedDelegationPage: 1,
2812
+ constrainedDelegationPage: 1,
2813
+ sidHistoryPage: 1,
2814
+ foreignPrincipalsPage: 1,
2815
+ gmsaVulnPage: 1,
2604
2816
  tabs: [
2605
2817
  {{ id: 'overview', label: 'Overview', icon: 'fas fa-home', count: null }},
2606
2818
  {{ id: 'findings', label: 'Security Findings', icon: 'fas fa-bug', count: null }},
@@ -3002,6 +3214,7 @@ class DashboardGenerator:
3002
3214
  if (this.objectsWithSIDHistory.length > 0) count++; // SID History
3003
3215
  if (this.foreignSecurityPrincipals.length > 0) count++; // Foreign Security Principals
3004
3216
  if (this.vulnerableGMSA.length > 0) count++; // gMSA Misconfigurations
3217
+ if (this.legacyOperatingSystems.length > 0) count++; // Legacy Operating Systems
3005
3218
  return count;
3006
3219
  }},
3007
3220
 
@@ -3607,6 +3820,459 @@ class DashboardGenerator:
3607
3820
  return vulnerable;
3608
3821
  }},
3609
3822
 
3823
+ // 5. LEGACY OPERATING SYSTEMS
3824
+ legacyOperatingSystems() {{
3825
+ let legacySystems = [];
3826
+
3827
+ // Current date for EOL calculations
3828
+ const now = new Date();
3829
+ const twelveMonthsFromNow = new Date(now.getTime() + (365 * 24 * 60 * 60 * 1000));
3830
+
3831
+ // Build number to version mapping
3832
+ const buildMapping = {{
3833
+ // Windows 11 builds
3834
+ '22000': '21H2',
3835
+ '22621': '22H2',
3836
+ '22631': '23H2',
3837
+ '26100': '24H2',
3838
+ '26200': '25H2',
3839
+ '28000': '26H1',
3840
+ // Windows 10 builds
3841
+ '10240': '1507',
3842
+ '10586': '1511',
3843
+ '14393': '1607',
3844
+ '15063': '1703',
3845
+ '16299': '1709',
3846
+ '17134': '1803',
3847
+ '17763': '1809',
3848
+ '18362': '1903',
3849
+ '18363': '1909',
3850
+ '19041': '2004',
3851
+ '19042': '20H2',
3852
+ '19043': '21H1',
3853
+ '19044': '21H2',
3854
+ '19045': '22H2'
3855
+ }};
3856
+
3857
+ // EOL database by OS and edition
3858
+ const eolDatabase = {{
3859
+ // Windows Server (by build number)
3860
+ 'Server 2003': {{ securityEnd: '2015-07-14', esuEnd: null }},
3861
+ 'Server 2008': {{ securityEnd: '2020-01-14', esuEnd: '2023-01-10' }},
3862
+ 'Server 2008 R2': {{ securityEnd: '2020-01-14', esuEnd: '2023-01-10' }},
3863
+ 'Server 2012': {{ securityEnd: '2023-10-10', esuEnd: '2026-10-13' }},
3864
+ 'Server 2012 R2': {{ securityEnd: '2023-10-10', esuEnd: '2026-10-13' }},
3865
+ 'Server 2016': {{ securityEnd: '2027-01-12', esuEnd: null }},
3866
+ // Server 2019 and later not included (support until 2029+)
3867
+
3868
+ // Windows client OS (by version and edition)
3869
+ 'XP': {{ securityEnd: '2014-04-08', esuEnd: null }},
3870
+ 'Vista': {{ securityEnd: '2017-04-11', esuEnd: null }},
3871
+ '7': {{ securityEnd: '2020-01-14', esuEnd: null }},
3872
+ '8': {{ securityEnd: '2016-01-12', esuEnd: null }},
3873
+ '8.1': {{ securityEnd: '2023-01-10', esuEnd: null }},
3874
+
3875
+ // Windows 10 by version
3876
+ '10-1507': {{ securityEnd: '2025-10-14', esuEnd: null }},
3877
+ '10-1507-E-LTSC': {{ securityEnd: '2025-10-14', esuEnd: null }},
3878
+ '10-1511': {{ securityEnd: '2017-10-10', esuEnd: null }},
3879
+ '10-1607': {{ securityEnd: '2026-10-13', esuEnd: null }},
3880
+ '10-1607-E-LTSC': {{ securityEnd: '2026-10-13', esuEnd: null }},
3881
+ '10-1703': {{ securityEnd: '2019-10-08', esuEnd: null }},
3882
+ '10-1709': {{ securityEnd: '2020-10-13', esuEnd: null }},
3883
+ '10-1803': {{ securityEnd: '2021-05-11', esuEnd: null }},
3884
+ '10-1809': {{ securityEnd: '2021-05-11', esuEnd: null }},
3885
+ '10-1809-E-LTSC': {{ securityEnd: '2029-01-09', esuEnd: null }}, // Still supported
3886
+ '10-1903': {{ securityEnd: '2020-12-08', esuEnd: null }},
3887
+ '10-1909': {{ securityEnd: '2022-05-10', esuEnd: null }},
3888
+ '10-2004': {{ securityEnd: '2021-12-14', esuEnd: null }},
3889
+ '10-20H2': {{ securityEnd: '2023-05-09', esuEnd: null }},
3890
+ '10-21H1': {{ securityEnd: '2022-12-13', esuEnd: null }},
3891
+ '10-21H2': {{ securityEnd: '2024-06-11', esuEnd: null }},
3892
+ '10-21H2-E-LTSC': {{ securityEnd: '2027-01-12', esuEnd: null }},
3893
+ '10-22H2': {{ securityEnd: '2025-10-14', esuEnd: '2028-10-10' }},
3894
+
3895
+ // Windows 11 by version and edition
3896
+ '11-21H2-Pro': {{ securityEnd: '2023-10-10', esuEnd: null }},
3897
+ '11-21H2-Enterprise': {{ securityEnd: '2024-10-08', esuEnd: null }},
3898
+ '11-22H2-Pro': {{ securityEnd: '2024-10-08', esuEnd: null }},
3899
+ '11-22H2-Enterprise': {{ securityEnd: '2025-10-14', esuEnd: null }},
3900
+ '11-23H2-Pro': {{ securityEnd: '2025-11-11', esuEnd: null }},
3901
+ '11-23H2-Enterprise': {{ securityEnd: '2026-11-10', esuEnd: null }},
3902
+ '11-24H2-Pro': {{ securityEnd: '2026-10-13', esuEnd: null }},
3903
+ '11-24H2-Enterprise': {{ securityEnd: '2027-10-12', esuEnd: null }},
3904
+ '11-25H2-Pro': {{ securityEnd: '2027-10-12', esuEnd: null }},
3905
+ '11-25H2-Enterprise': {{ securityEnd: '2028-10-10', esuEnd: null }},
3906
+ // 26H1 and later not included (support until 2028+)
3907
+
3908
+ // Ubuntu LTS
3909
+ 'Ubuntu 16.04': {{ securityEnd: '2021-04-02', esuEnd: '2026-04-02' }},
3910
+ 'Ubuntu 18.04': {{ securityEnd: '2023-05-31', esuEnd: '2028-04-01' }},
3911
+ 'Ubuntu 20.04': {{ securityEnd: '2025-05-31', esuEnd: '2030-04-02' }},
3912
+ 'Ubuntu 22.04': {{ securityEnd: '2027-04-01', esuEnd: '2032-04-09' }},
3913
+
3914
+ // Debian
3915
+ 'Debian 9': {{ securityEnd: '2020-07-18', esuEnd: '2022-07-01' }},
3916
+ 'Debian 10': {{ securityEnd: '2022-09-10', esuEnd: '2024-06-30' }},
3917
+ 'Debian 11': {{ securityEnd: '2024-08-14', esuEnd: '2026-08-31' }},
3918
+ 'Debian 12': {{ securityEnd: '2026-06-10', esuEnd: '2028-06-30' }}
3919
+ }};
3920
+
3921
+ this.computers.forEach(computer => {{
3922
+ const os = computer['Operating System'] || '';
3923
+ const enabled = computer.Enabled === 'True' || computer.Enabled === 'TRUE';
3924
+
3925
+ let eolInfo = null;
3926
+ let osDisplayName = '';
3927
+
3928
+ // Extract build number from format like "10.0 (17763)"
3929
+ const buildMatch = os.match(/\\((\\d{{5}})\\)/);
3930
+ const buildNumber = buildMatch ? buildMatch[1] : null;
3931
+ const version = buildNumber ? buildMapping[buildNumber] : null;
3932
+
3933
+ // Detect OS type and edition
3934
+ if (/Windows Server 2003/i.test(os)) {{
3935
+ eolInfo = eolDatabase['Server 2003'];
3936
+ osDisplayName = 'Windows Server 2003';
3937
+ }}
3938
+ else if (/Windows Server 2008 R2/i.test(os)) {{
3939
+ eolInfo = eolDatabase['Server 2008 R2'];
3940
+ osDisplayName = 'Windows Server 2008 R2';
3941
+ }}
3942
+ else if (/Windows Server 2008(?! R2)/i.test(os)) {{
3943
+ eolInfo = eolDatabase['Server 2008'];
3944
+ osDisplayName = 'Windows Server 2008';
3945
+ }}
3946
+ else if (/Windows Server 2012 R2/i.test(os)) {{
3947
+ eolInfo = eolDatabase['Server 2012 R2'];
3948
+ osDisplayName = 'Windows Server 2012 R2';
3949
+ }}
3950
+ else if (/Windows Server 2012(?! R2)/i.test(os)) {{
3951
+ eolInfo = eolDatabase['Server 2012'];
3952
+ osDisplayName = 'Windows Server 2012';
3953
+ }}
3954
+ else if (/Windows Server 2016/i.test(os)) {{
3955
+ eolInfo = eolDatabase['Server 2016'];
3956
+ osDisplayName = 'Windows Server 2016';
3957
+ }}
3958
+ // Server 2019, 2022, 2025 not included (support until 2029+)
3959
+
3960
+ else if (/Windows 11/i.test(os) && version) {{
3961
+ const isEnterprise = /Enterprise/i.test(os);
3962
+ const edition = isEnterprise ? 'Enterprise' : 'Pro';
3963
+ const key = `11-${{version}}-${{edition}}`;
3964
+ eolInfo = eolDatabase[key];
3965
+ osDisplayName = `Windows 11 ${{version}} (${{edition}})`;
3966
+ }}
3967
+ else if (/Windows 10/i.test(os) && version) {{
3968
+ const isEnterprise = /Enterprise/i.test(os);
3969
+ const isLTSC = /LTSC/i.test(os);
3970
+ let key = `10-${{version}}`;
3971
+ if (isEnterprise && isLTSC) {{
3972
+ key = `10-${{version}}-E-LTSC`;
3973
+ }}
3974
+ eolInfo = eolDatabase[key];
3975
+ osDisplayName = isLTSC ? `Windows 10 ${{version}} LTSC` : `Windows 10 ${{version}}`;
3976
+ }}
3977
+ else if (/Windows XP/i.test(os)) {{
3978
+ eolInfo = eolDatabase['XP'];
3979
+ osDisplayName = 'Windows XP';
3980
+ }}
3981
+ else if (/Windows Vista/i.test(os)) {{
3982
+ eolInfo = eolDatabase['Vista'];
3983
+ osDisplayName = 'Windows Vista';
3984
+ }}
3985
+ else if (/Windows 7/i.test(os)) {{
3986
+ eolInfo = eolDatabase['7'];
3987
+ osDisplayName = 'Windows 7';
3988
+ }}
3989
+ else if (/Windows 8\\.1/i.test(os)) {{
3990
+ eolInfo = eolDatabase['8.1'];
3991
+ osDisplayName = 'Windows 8.1';
3992
+ }}
3993
+ else if (/Windows 8(?!\\.1)/i.test(os)) {{
3994
+ eolInfo = eolDatabase['8'];
3995
+ osDisplayName = 'Windows 8';
3996
+ }}
3997
+ else if (/Ubuntu.*16\\.04/i.test(os)) {{
3998
+ eolInfo = eolDatabase['Ubuntu 16.04'];
3999
+ osDisplayName = 'Ubuntu 16.04 LTS';
4000
+ }}
4001
+ else if (/Ubuntu.*18\\.04/i.test(os)) {{
4002
+ eolInfo = eolDatabase['Ubuntu 18.04'];
4003
+ osDisplayName = 'Ubuntu 18.04 LTS';
4004
+ }}
4005
+ else if (/Ubuntu.*20\\.04/i.test(os)) {{
4006
+ eolInfo = eolDatabase['Ubuntu 20.04'];
4007
+ osDisplayName = 'Ubuntu 20.04 LTS';
4008
+ }}
4009
+ else if (/Ubuntu.*22\\.04/i.test(os)) {{
4010
+ eolInfo = eolDatabase['Ubuntu 22.04'];
4011
+ osDisplayName = 'Ubuntu 22.04 LTS';
4012
+ }}
4013
+ else if (/Debian.*9/i.test(os)) {{
4014
+ eolInfo = eolDatabase['Debian 9'];
4015
+ osDisplayName = 'Debian 9 (Stretch)';
4016
+ }}
4017
+ else if (/Debian.*10/i.test(os)) {{
4018
+ eolInfo = eolDatabase['Debian 10'];
4019
+ osDisplayName = 'Debian 10 (Buster)';
4020
+ }}
4021
+ else if (/Debian.*11/i.test(os)) {{
4022
+ eolInfo = eolDatabase['Debian 11'];
4023
+ osDisplayName = 'Debian 11 (Bullseye)';
4024
+ }}
4025
+ else if (/Debian.*12/i.test(os)) {{
4026
+ eolInfo = eolDatabase['Debian 12'];
4027
+ osDisplayName = 'Debian 12 (Bookworm)';
4028
+ }}
4029
+
4030
+ // If we found EOL info, check if it should be flagged
4031
+ if (eolInfo) {{
4032
+ const securityEndDate = new Date(eolInfo.securityEnd);
4033
+ const esuEndDate = eolInfo.esuEnd ? new Date(eolInfo.esuEnd) : null;
4034
+
4035
+ let shouldFlag = false;
4036
+ let riskLevel = 'LOW';
4037
+ let eolStatus = '';
4038
+ let relevantEndDate = securityEndDate;
4039
+
4040
+ // CRITICAL: Security support ended
4041
+ if (now > securityEndDate) {{
4042
+ shouldFlag = true;
4043
+ riskLevel = 'CRITICAL';
4044
+ eolStatus = 'End-of-Life';
4045
+ relevantEndDate = securityEndDate;
4046
+ }}
4047
+ // INFO: Security support ends within 12 months
4048
+ else if (securityEndDate <= twelveMonthsFromNow) {{
4049
+ shouldFlag = true;
4050
+ riskLevel = 'INFO';
4051
+ eolStatus = 'SOON EOL';
4052
+ relevantEndDate = securityEndDate;
4053
+ }}
4054
+
4055
+ // Adjust risk for disabled systems (reduce by one level)
4056
+ if (!enabled && shouldFlag) {{
4057
+ if (riskLevel === 'CRITICAL') riskLevel = 'HIGH';
4058
+ else if (riskLevel === 'INFO') riskLevel = 'LOW';
4059
+ }}
4060
+
4061
+ // Only add to list if we should flag it
4062
+ if (shouldFlag) {{
4063
+ // Calculate days until/since EOL
4064
+ const daysDiff = Math.floor((now - relevantEndDate) / (1000 * 60 * 60 * 24));
4065
+
4066
+ // For SOON EOL (negative days means future EOL), show countdown
4067
+ let daysDisplay;
4068
+ if (daysDiff > 0) {{
4069
+ // Already EOL - show positive days
4070
+ daysDisplay = daysDiff;
4071
+ }} else {{
4072
+ // SOON EOL - show negative days (countdown)
4073
+ daysDisplay = daysDiff; // This will be negative
4074
+ }}
4075
+
4076
+ // Format EOL date
4077
+ const eolDateStr = relevantEndDate.toISOString().split('T')[0];
4078
+
4079
+ legacySystems.push({{
4080
+ Name: computer.Name || computer.UserName,
4081
+ 'DNS Hostname': computer.DNSHostName || computer['DNS Hostname'] || '',
4082
+ 'Operating System': os,
4083
+ 'OS Version': osDisplayName,
4084
+ 'IPv4Address': computer.IPv4Address || computer['IPv4 Address'] || '',
4085
+ Enabled: enabled ? 'Yes' : 'No',
4086
+ 'EOL Date': eolDateStr,
4087
+ 'Days Since EOL': daysDisplay,
4088
+ 'EOL Status': eolStatus,
4089
+ 'Logon Age (days)': computer['Logon Age (days)'] || '',
4090
+ 'Risk Level': riskLevel,
4091
+ DN: computer['Distinguished Name'] || computer.DistinguishedName || ''
4092
+ }});
4093
+ }}
4094
+ }}
4095
+ }});
4096
+
4097
+ // Apply sorting
4098
+ if (this.legacyOsSortColumn) {{
4099
+ const direction = this.legacyOsSortDirection === 'asc' ? 1 : -1;
4100
+ const column = this.legacyOsSortColumn;
4101
+
4102
+ legacySystems = [...legacySystems].sort((a, b) => {{
4103
+ if (column === 'Risk Level') {{
4104
+ const riskPriority = {{ 'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'INFO': 3, 'LOW': 4 }};
4105
+ const aPriority = riskPriority[a[column]] ?? 999;
4106
+ const bPriority = riskPriority[b[column]] ?? 999;
4107
+ if (aPriority !== bPriority) {{
4108
+ return (aPriority - bPriority) * direction;
4109
+ }}
4110
+ // If risk levels are equal, sort by days since EOL as tiebreaker
4111
+ const aDays = parseInt(a['Days Since EOL']) || 0;
4112
+ const bDays = parseInt(b['Days Since EOL']) || 0;
4113
+ if (aDays !== bDays) {{
4114
+ return (bDays - aDays) * direction;
4115
+ }}
4116
+ // Final tiebreaker: sort by computer name
4117
+ const aName = (a['Name'] || '').toString().toLowerCase();
4118
+ const bName = (b['Name'] || '').toString().toLowerCase();
4119
+ if (aName < bName) return -1 * direction;
4120
+ if (aName > bName) return 1 * direction;
4121
+ return 0;
4122
+ }}
4123
+
4124
+ if (column === 'EOL Status') {{
4125
+ // Priority sort: End-of-Life (CRITICAL) > SOON EOL (INFO)
4126
+ const statusPriority = {{ 'End-of-Life': 0, 'SOON EOL': 1 }};
4127
+ const aPriority = statusPriority[a[column]] ?? 999;
4128
+ const bPriority = statusPriority[b[column]] ?? 999;
4129
+ if (aPriority !== bPriority) {{
4130
+ return (aPriority - bPriority) * direction;
4131
+ }}
4132
+ // If status is equal, sort by days since EOL as tiebreaker
4133
+ const aDays = parseInt(a['Days Since EOL']) || 0;
4134
+ const bDays = parseInt(b['Days Since EOL']) || 0;
4135
+ if (aDays !== bDays) {{
4136
+ return (bDays - aDays) * direction;
4137
+ }}
4138
+ // Final tiebreaker: sort by computer name
4139
+ const aName = (a['Name'] || '').toString().toLowerCase();
4140
+ const bName = (b['Name'] || '').toString().toLowerCase();
4141
+ if (aName < bName) return -1 * direction;
4142
+ if (aName > bName) return 1 * direction;
4143
+ return 0;
4144
+ }}
4145
+
4146
+ if (column === 'Days Since EOL') {{
4147
+ const aValue = parseInt(a[column]) || 0;
4148
+ const bValue = parseInt(b[column]) || 0;
4149
+
4150
+ // Sorting logic for mixed positive (past EOL) and negative (SOON EOL) values:
4151
+ // Ascending: Most critical first (past EOL with highest days > SOON EOL with lowest days until)
4152
+ // - Past EOL (positive): Higher values more critical (e.g., 365 > 30 since EOL)
4153
+ // - SOON EOL (negative): Less negative more critical (e.g., -30 > -365 until EOL)
4154
+ // Descending: Least critical first (reverse of above)
4155
+
4156
+ // For ascending (direction = 1), we want: bValue - aValue (descending numeric order)
4157
+ // Examples with ascending:
4158
+ // 365 vs 30: 30 - 365 = -335 (365 comes first) ✓
4159
+ // -30 vs -365: -365 - (-30) = -335 (-30 comes first) ✓
4160
+ // 365 vs -30: -30 - 365 = -395 (365 comes first) ✓
4161
+ if (aValue !== bValue) {{
4162
+ return (bValue - aValue) * direction;
4163
+ }}
4164
+ // Tiebreaker: sort by computer name
4165
+ const aName = (a['Name'] || '').toString().toLowerCase();
4166
+ const bName = (b['Name'] || '').toString().toLowerCase();
4167
+ if (aName < bName) return -1 * direction;
4168
+ if (aName > bName) return 1 * direction;
4169
+ return 0;
4170
+ }}
4171
+
4172
+ if (column === 'Logon Age (days)') {{
4173
+ // Handle empty, null, undefined, and parse numeric values
4174
+ let aValue = 999999; // Default for "never logged in"
4175
+ let bValue = 999999;
4176
+
4177
+ if (a[column] && a[column] !== '') {{
4178
+ const parsed = parseInt(a[column]);
4179
+ if (!isNaN(parsed)) {{
4180
+ aValue = parsed;
4181
+ }}
4182
+ }}
4183
+
4184
+ if (b[column] && b[column] !== '') {{
4185
+ const parsed = parseInt(b[column]);
4186
+ if (!isNaN(parsed)) {{
4187
+ bValue = parsed;
4188
+ }}
4189
+ }}
4190
+
4191
+ if (aValue !== bValue) {{
4192
+ return (aValue - bValue) * direction;
4193
+ }}
4194
+ // Tiebreaker: sort by computer name
4195
+ const aName = (a['Name'] || '').toString().toLowerCase();
4196
+ const bName = (b['Name'] || '').toString().toLowerCase();
4197
+ if (aName < bName) return -1 * direction;
4198
+ if (aName > bName) return 1 * direction;
4199
+ return 0;
4200
+ }}
4201
+
4202
+ if (column === 'EOL Date') {{
4203
+ // Sort dates properly
4204
+ const aValue = a[column] || '';
4205
+ const bValue = b[column] || '';
4206
+ if (aValue !== bValue) {{
4207
+ if (aValue < bValue) return -1 * direction;
4208
+ if (aValue > bValue) return 1 * direction;
4209
+ }}
4210
+ // Tiebreaker: sort by computer name
4211
+ const aName = (a['Name'] || '').toString().toLowerCase();
4212
+ const bName = (b['Name'] || '').toString().toLowerCase();
4213
+ if (aName < bName) return -1 * direction;
4214
+ if (aName > bName) return 1 * direction;
4215
+ return 0;
4216
+ }}
4217
+
4218
+ const aValue = (a[column] || '').toString().toLowerCase();
4219
+ const bValue = (b[column] || '').toString().toLowerCase();
4220
+
4221
+ if (aValue !== bValue) {{
4222
+ if (aValue < bValue) return -1 * direction;
4223
+ if (aValue > bValue) return 1 * direction;
4224
+ }}
4225
+ // Tiebreaker: sort by computer name (unless already sorting by Name)
4226
+ if (column !== 'Name') {{
4227
+ const aName = (a['Name'] || '').toString().toLowerCase();
4228
+ const bName = (b['Name'] || '').toString().toLowerCase();
4229
+ if (aName < bName) return -1 * direction;
4230
+ if (aName > bName) return 1 * direction;
4231
+ }}
4232
+ return 0;
4233
+ }});
4234
+ }}
4235
+
4236
+ // Filter out disabled systems
4237
+ return legacySystems.filter(s => s.Enabled === 'Yes');
4238
+ }},
4239
+
4240
+ paginatedLegacyOS() {{
4241
+ const start = (this.legacyOsPage - 1) * this.itemsPerPage;
4242
+ const end = start + this.itemsPerPage;
4243
+ return this.legacyOperatingSystems.slice(start, end);
4244
+ }},
4245
+
4246
+ paginatedUnconstrainedDelegation() {{
4247
+ const start = (this.unconstrainedDelegationPage - 1) * this.itemsPerPage;
4248
+ const end = start + this.itemsPerPage;
4249
+ return this.unconstrainedDelegationAccounts.slice(start, end);
4250
+ }},
4251
+
4252
+ paginatedConstrainedDelegation() {{
4253
+ const start = (this.constrainedDelegationPage - 1) * this.itemsPerPage;
4254
+ const end = start + this.itemsPerPage;
4255
+ return this.constrainedDelegationRisks.slice(start, end);
4256
+ }},
4257
+
4258
+ paginatedSIDHistory() {{
4259
+ const start = (this.sidHistoryPage - 1) * this.itemsPerPage;
4260
+ const end = start + this.itemsPerPage;
4261
+ return this.objectsWithSIDHistory.slice(start, end);
4262
+ }},
4263
+
4264
+ paginatedForeignPrincipals() {{
4265
+ const start = (this.foreignPrincipalsPage - 1) * this.itemsPerPage;
4266
+ const end = start + this.itemsPerPage;
4267
+ return this.foreignSecurityPrincipals.slice(start, end);
4268
+ }},
4269
+
4270
+ paginatedGMSAVuln() {{
4271
+ const start = (this.gmsaVulnPage - 1) * this.itemsPerPage;
4272
+ const end = start + this.itemsPerPage;
4273
+ return this.vulnerableGMSA.slice(start, end);
4274
+ }},
4275
+
3610
4276
  criticalFindings() {{
3611
4277
  let findings = [];
3612
4278
 
@@ -4264,6 +4930,15 @@ class DashboardGenerator:
4264
4930
  }}
4265
4931
  }},
4266
4932
 
4933
+ sortLegacyOS(column) {{
4934
+ if (this.legacyOsSortColumn === column) {{
4935
+ this.legacyOsSortDirection = this.legacyOsSortDirection === 'asc' ? 'desc' : 'asc';
4936
+ }} else {{
4937
+ this.legacyOsSortColumn = column;
4938
+ this.legacyOsSortDirection = 'asc';
4939
+ }}
4940
+ }},
4941
+
4267
4942
  initCharts() {{
4268
4943
  // User Status Chart - Comprehensive Overview
4269
4944
  const userStatusCtx = document.getElementById('userStatusChart');
@@ -0,0 +1,3 @@
1
+ {
2
+ "version": "0.13.3"
3
+ }
@@ -104,7 +104,7 @@ if not KERBEROS_AVAILABLE:
104
104
 
105
105
 
106
106
  # Constants
107
- VERSION = "v0.13.2" # Automatically updated by CI/CD pipeline during release
107
+ VERSION = "v0.13.3" # Automatically updated by CI/CD pipeline during release
108
108
  BANNER = f"""
109
109
  ╔═════════════════════════════════════════════════════════
110
110
  ║ PyADRecon {VERSION} - Python AD Reconnaissance Tool
@@ -1,3 +0,0 @@
1
- {
2
- "version": "0.13.2"
3
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes