dissect.database 1.2.dev1__tar.gz → 1.2.dev3__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 (218) hide show
  1. {dissect_database-1.2.dev1/dissect.database.egg-info → dissect_database-1.2.dev3}/PKG-INFO +1 -1
  2. dissect_database-1.2.dev3/dissect/database/ese/cursor.py +433 -0
  3. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/database.py +10 -3
  4. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/ntds.py +47 -2
  5. dissect_database-1.2.dev3/dissect/database/ese/ntds/objects/secret.py +99 -0
  6. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/query.py +56 -59
  7. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/page.py +4 -4
  8. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/table.py +6 -10
  9. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3/dissect.database.egg-info}/PKG-INFO +1 -1
  10. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/SOURCES.txt +0 -1
  11. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_ntds.py +28 -0
  12. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_query.py +19 -8
  13. dissect_database-1.2.dev1/dissect/database/ese/btree.py +0 -177
  14. dissect_database-1.2.dev1/dissect/database/ese/cursor.py +0 -230
  15. dissect_database-1.2.dev1/dissect/database/ese/ntds/objects/secret.py +0 -13
  16. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/COPYRIGHT +0 -0
  17. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/LICENSE +0 -0
  18. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/MANIFEST.in +0 -0
  19. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/README.md +0 -0
  20. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/__init__.py +0 -0
  21. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/__init__.py +0 -0
  22. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/c_db.py +0 -0
  23. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/c_db.pyi +0 -0
  24. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/db.py +0 -0
  25. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/tools/__init__.py +0 -0
  26. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/tools/c_rpm.py +0 -0
  27. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/tools/c_rpm.pyi +0 -0
  28. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/tools/rpm.py +0 -0
  29. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/__init__.py +0 -0
  30. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/c_ese.py +0 -0
  31. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/c_ese.pyi +0 -0
  32. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/compression.py +0 -0
  33. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ese.py +0 -0
  34. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/exception.py +0 -0
  35. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/index.py +0 -0
  36. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/lcmapstring.py +0 -0
  37. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/__init__.py +0 -0
  38. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_ds.py +0 -0
  39. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_ds.pyi +0 -0
  40. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_pek.py +0 -0
  41. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_pek.pyi +0 -0
  42. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_sd.py +0 -0
  43. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_sd.pyi +0 -0
  44. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/__init__.py +0 -0
  45. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/applicationsettings.py +0 -0
  46. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/attributeschema.py +0 -0
  47. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/builtindomain.py +0 -0
  48. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/certificationauthority.py +0 -0
  49. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/classschema.py +0 -0
  50. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/classstore.py +0 -0
  51. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/computer.py +0 -0
  52. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/configuration.py +0 -0
  53. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/container.py +0 -0
  54. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/controlaccessright.py +0 -0
  55. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/crldistributionpoint.py +0 -0
  56. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/crossref.py +0 -0
  57. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/crossrefcontainer.py +0 -0
  58. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dfsconfiguration.py +0 -0
  59. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/displayspecifier.py +0 -0
  60. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dmd.py +0 -0
  61. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dnsnode.py +0 -0
  62. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dnszone.py +0 -0
  63. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/domain.py +0 -0
  64. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/domaindns.py +0 -0
  65. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/domainpolicy.py +0 -0
  66. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dsuisettings.py +0 -0
  67. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/filelinktracking.py +0 -0
  68. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/foreignsecurityprincipal.py +0 -0
  69. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/group.py +0 -0
  70. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/grouppolicycontainer.py +0 -0
  71. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/infrastructureupdate.py +0 -0
  72. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/intersitetransport.py +0 -0
  73. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/intersitetransportcontainer.py +0 -0
  74. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecbase.py +0 -0
  75. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecfilter.py +0 -0
  76. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecisakmppolicy.py +0 -0
  77. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecnegotiationpolicy.py +0 -0
  78. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecnfa.py +0 -0
  79. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecpolicy.py +0 -0
  80. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/leaf.py +0 -0
  81. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/linktrackobjectmovetable.py +0 -0
  82. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/linktrackvolumetable.py +0 -0
  83. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/locality.py +0 -0
  84. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/lostandfound.py +0 -0
  85. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msauthz_centralaccesspolicies.py +0 -0
  86. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msauthz_centralaccessrules.py +0 -0
  87. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_content.py +0 -0
  88. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_contentset.py +0 -0
  89. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_globalsettings.py +0 -0
  90. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_localsettings.py +0 -0
  91. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_member.py +0 -0
  92. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_replicationgroup.py +0 -0
  93. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_subscriber.py +0 -0
  94. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_subscription.py +0 -0
  95. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_topology.py +0 -0
  96. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdns_serversettings.py +0 -0
  97. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_authnpolicies.py +0 -0
  98. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_authnpolicysilos.py +0 -0
  99. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_claimstransformationpolicies.py +0 -0
  100. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_claimtype.py +0 -0
  101. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_claimtypepropertybase.py +0 -0
  102. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_claimtypes.py +0 -0
  103. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_optionalfeature.py +0 -0
  104. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_passwordsettingscontainer.py +0 -0
  105. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_quotacontainer.py +0 -0
  106. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_resourceproperties.py +0 -0
  107. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_resourceproperty.py +0 -0
  108. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_resourcepropertylist.py +0 -0
  109. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_shadowprincipalcontainer.py +0 -0
  110. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_valuetype.py +0 -0
  111. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msimaging_psps.py +0 -0
  112. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/mskds_provserverconfiguration.py +0 -0
  113. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msmqenterprisesettings.py +0 -0
  114. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/mspki_enterpriseoid.py +0 -0
  115. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/mspki_privatekeyrecoveryagent.py +0 -0
  116. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msspp_activationobjectscontainer.py +0 -0
  117. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/mstpm_informationobjectscontainer.py +0 -0
  118. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntdsconnection.py +0 -0
  119. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntdsdsa.py +0 -0
  120. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntdsservice.py +0 -0
  121. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntdssitesettings.py +0 -0
  122. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntfrssettings.py +0 -0
  123. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/object.py +0 -0
  124. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/organizationalperson.py +0 -0
  125. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/organizationalunit.py +0 -0
  126. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/person.py +0 -0
  127. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/physicallocation.py +0 -0
  128. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/pkicertificatetemplate.py +0 -0
  129. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/pkienrollmentservice.py +0 -0
  130. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/querypolicy.py +0 -0
  131. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ridmanager.py +0 -0
  132. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ridset.py +0 -0
  133. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/rpccontainer.py +0 -0
  134. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/rrasadministrationdictionary.py +0 -0
  135. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/samserver.py +0 -0
  136. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/securityobject.py +0 -0
  137. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/server.py +0 -0
  138. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/serverscontainer.py +0 -0
  139. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/site.py +0 -0
  140. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/sitelink.py +0 -0
  141. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/sitescontainer.py +0 -0
  142. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/subnetcontainer.py +0 -0
  143. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/subschema.py +0 -0
  144. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/top.py +0 -0
  145. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/trusteddomain.py +0 -0
  146. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/user.py +0 -0
  147. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/pek.py +0 -0
  148. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/schema.py +0 -0
  149. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/sd.py +0 -0
  150. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/tools/__init__.py +0 -0
  151. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/tools/ntds.py +0 -0
  152. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/util.py +0 -0
  153. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/record.py +0 -0
  154. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/sorting_table.py +0 -0
  155. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/__init__.py +0 -0
  156. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/certlog.py +0 -0
  157. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/impacket.py +0 -0
  158. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/sru.py +0 -0
  159. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/ual.py +0 -0
  160. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/util.py +0 -0
  161. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/exception.py +0 -0
  162. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/__init__.py +0 -0
  163. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/c_sqlite3.py +0 -0
  164. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/c_sqlite3.pyi +0 -0
  165. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/encryption/__init__.py +0 -0
  166. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/encryption/sqlcipher/__init__.py +0 -0
  167. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/encryption/sqlcipher/exception.py +0 -0
  168. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/encryption/sqlcipher/sqlcipher.py +0 -0
  169. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/exception.py +0 -0
  170. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/sqlite3.py +0 -0
  171. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/util.py +0 -0
  172. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/wal.py +0 -0
  173. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/dependency_links.txt +0 -0
  174. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/entry_points.txt +0 -0
  175. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/requires.txt +0 -0
  176. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/top_level.txt +0 -0
  177. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/pyproject.toml +0 -0
  178. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/setup.cfg +0 -0
  179. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/__init__.py +0 -0
  180. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_docs/Makefile +0 -0
  181. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_docs/conf.py +0 -0
  182. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_docs/index.rst +0 -0
  183. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_tools/sqlite3/__init__.py +0 -0
  184. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_tools/sqlite3/generate_sqlite.py +0 -0
  185. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_util.py +0 -0
  186. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/bsd/__init__.py +0 -0
  187. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/bsd/conftest.py +0 -0
  188. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/bsd/test_db.py +0 -0
  189. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/bsd/test_rpm.py +0 -0
  190. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/conftest.py +0 -0
  191. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/__init__.py +0 -0
  192. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/conftest.py +0 -0
  193. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/__init__.py +0 -0
  194. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/conftest.py +0 -0
  195. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_benchmark.py +0 -0
  196. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_pek.py +0 -0
  197. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_schema.py +0 -0
  198. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_sd.py +0 -0
  199. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_util.py +0 -0
  200. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_cursor.py +0 -0
  201. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_ese.py +0 -0
  202. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_index.py +0 -0
  203. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_page.py +0 -0
  204. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_record.py +0 -0
  205. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_table.py +0 -0
  206. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/tools/__init__.py +0 -0
  207. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/tools/test_certlog.py +0 -0
  208. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/tools/test_sru.py +0 -0
  209. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/tools/test_ual.py +0 -0
  210. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/__init__.py +0 -0
  211. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/conftest.py +0 -0
  212. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_default_values.py +0 -0
  213. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_row.py +0 -0
  214. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_sqlcipher.py +0 -0
  215. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_sqlite3.py +0 -0
  216. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_util.py +0 -0
  217. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_wal.py +0 -0
  218. {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.database
3
- Version: 1.2.dev1
3
+ Version: 1.2.dev3
4
4
  Summary: A Dissect module implementing parsers for various database formats, including Berkeley DB, Microsofts Extensible Storage Engine (ESE) and SQLite3
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,433 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from dissect.database.ese.exception import KeyNotFoundError
6
+ from dissect.database.ese.record import Record
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Iterator
10
+
11
+ from typing_extensions import Self
12
+
13
+ from dissect.database.ese.ese import ESE
14
+ from dissect.database.ese.index import Index
15
+ from dissect.database.ese.page import Node, Page
16
+ from dissect.database.ese.util import RecordValue
17
+
18
+
19
+ class Cursor:
20
+ """A simple cursor implementation for searching the ESE indexes on their records.
21
+
22
+ Args:
23
+ index: The :class:`~dissect.database.ese.index.Index` to create the cursor for.
24
+ """
25
+
26
+ def __init__(self, index: Index):
27
+ self.index = index
28
+ self.table = index.table
29
+ self.db = index.db
30
+
31
+ self._primary = RawCursor(self.db, index.root)
32
+ self._secondary = None if index.is_primary else RawCursor(self.db, self.table.root)
33
+
34
+ def __iter__(self) -> Iterator[Record]:
35
+ if self._primary._page.is_branch:
36
+ self._primary.first()
37
+
38
+ record = self.record()
39
+ while record is not None:
40
+ yield record
41
+ record = self.next()
42
+
43
+ def _node(self) -> Node:
44
+ """Return the node the cursor is currently on. Resolves the secondary index if needed.
45
+
46
+ Returns:
47
+ A :class:`~dissect.database.ese.page.Node` object of the current node.
48
+ """
49
+ node = self._primary.node()
50
+ if self._secondary is not None:
51
+ node = self._secondary.search(node.data.tobytes(), exact=True).node()
52
+ return node
53
+
54
+ def record(self) -> Record:
55
+ """Return the record the cursor is currently on.
56
+
57
+ Returns:
58
+ A :class:`~dissect.database.ese.record.Record` object of the current record.
59
+ """
60
+ return Record(self.table, self._node())
61
+
62
+ def reset(self) -> Self:
63
+ """Reset the internal state."""
64
+ self._primary.reset()
65
+ if self._secondary:
66
+ self._secondary.reset()
67
+ return self
68
+
69
+ def next(self) -> Record | None:
70
+ """Move the cursor to the next record and return it.
71
+
72
+ Returns:
73
+ A :class:`~dissect.database.ese.record.Record` object of the next record.
74
+ """
75
+ if self._primary.next():
76
+ return self.record()
77
+ return None
78
+
79
+ def prev(self) -> Record | None:
80
+ """Move the cursor to the previous node and return it.
81
+
82
+ Returns:
83
+ A :class:`~dissect.database.ese.record.Record` object of the previous record.
84
+ """
85
+ if self._primary.prev():
86
+ return self.record()
87
+ return None
88
+
89
+ def make_key(self, *args: RecordValue, **kwargs: RecordValue) -> bytes:
90
+ """Generate a key for this index from the given values.
91
+
92
+ Args:
93
+ *args: The values to generate a key for.
94
+ **kwargs: The columns and values to generate a key for.
95
+
96
+ Returns:
97
+ The generated key as bytes.
98
+ """
99
+ if not args and not kwargs:
100
+ raise ValueError("At least one value must be provided")
101
+
102
+ if args and kwargs:
103
+ raise ValueError("Cannot mix positional and keyword arguments in make_key")
104
+
105
+ if args and not len(args) == 1 and not isinstance(args[0], list):
106
+ raise ValueError("When using positional arguments, provide a single list of values")
107
+
108
+ return self.index.make_key(args[0] if args else kwargs)
109
+
110
+ def search(self, *args: RecordValue, **kwargs: RecordValue) -> Record:
111
+ """Search the index for the requested values.
112
+
113
+ Searching modifies the cursor state. Searching again will search from the current position.
114
+ Reset the cursor with :meth:`reset` to start from the beginning.
115
+
116
+ Args:
117
+ *args: The values to search for.
118
+ **kwargs: The columns and values to search for.
119
+
120
+ Returns:
121
+ A :class:`~dissect.database.ese.record.Record` object of the found record.
122
+ """
123
+ key = self.make_key(*args, **kwargs)
124
+ return self.search_key(key, exact=True)
125
+
126
+ def search_key(self, key: bytes, exact: bool = True) -> Record:
127
+ """Search for a record with the given ``key``.
128
+
129
+ Args:
130
+ key: The key to search for.
131
+ exact: If ``True``, search for an exact match. If ``False``, sets the cursor on the
132
+ next record that is greater than or equal to the key.
133
+ """
134
+ self._primary.search(key, exact=exact)
135
+ return self.record()
136
+
137
+ def seek(self, *args: RecordValue, **kwargs: RecordValue) -> Self:
138
+ """Seek to the record with the given values.
139
+
140
+ Args:
141
+ *args: The values to seek to.
142
+ **kwargs: The columns and values to seek to.
143
+ """
144
+ key = self.make_key(*args, **kwargs)
145
+ self.search_key(key, exact=False)
146
+ return self
147
+
148
+ def seek_key(self, key: bytes) -> Self:
149
+ """Seek to the record with the given ``key``.
150
+
151
+ Args:
152
+ key: The key to seek to.
153
+ """
154
+ self._primary.search(key, exact=False)
155
+ return self
156
+
157
+ def find(self, **kwargs: RecordValue) -> Record | None:
158
+ """Find a record in the index.
159
+
160
+ This differs from :meth:`search` in that it will allow additional filtering on non-indexed columns.
161
+
162
+ Args:
163
+ **kwargs: The columns and values to search for.
164
+ """
165
+ return next(self.find_all(**kwargs), None)
166
+
167
+ def find_all(self, **kwargs: RecordValue) -> Iterator[Record]:
168
+ """Find all records in the index that match the given values.
169
+
170
+ This differs from :meth:`search` in that it will allows additional filtering on non-indexed columns.
171
+ If you only search on indexed columns, this will yield all records that match the indexed columns.
172
+
173
+ Args:
174
+ **kwargs: The columns and values to search for.
175
+ """
176
+ indexed_columns = {c.name: kwargs.pop(c.name) for c in self.index.columns}
177
+ other_columns = kwargs
178
+
179
+ # We need at least an exact match on the indexed columns
180
+ try:
181
+ self.search(**indexed_columns)
182
+ except KeyNotFoundError:
183
+ return
184
+
185
+ current_key = self._primary.node().key
186
+ while True:
187
+ # Entries with the same indexed columns are guaranteed to be adjacent
188
+ if current_key != self._primary.node().key:
189
+ break
190
+
191
+ record = self.record()
192
+ for k, v in other_columns.items():
193
+ value = record.get(k)
194
+ # If the record value is a list, we do a check based on the queried value
195
+ if isinstance(value, list):
196
+ # If the queried value is also a list, we check if they are equal
197
+ if isinstance(v, list):
198
+ if value != v:
199
+ break
200
+ # Otherwise we check if the queried value is in the record value
201
+ elif v not in value:
202
+ break
203
+ else:
204
+ if value != v:
205
+ break
206
+ else:
207
+ yield record
208
+
209
+ if not self._primary.next():
210
+ break
211
+
212
+
213
+ class RawCursor:
214
+ """A simple cursor implementation for searching the ESE B+Trees on their raw nodes.
215
+
216
+ Args:
217
+ db: An instance of :class:`~dissect.database.ese.ese.ESE`.
218
+ root: The page to open the raw cursor on.
219
+ """
220
+
221
+ def __init__(self, db: ESE, root: Page | int):
222
+ self.db = db
223
+ self.root = db.page(root) if isinstance(root, int) else root
224
+
225
+ self._page = self.root
226
+ self._idx = 0
227
+
228
+ # Stack of (page, idx, stack[:]) for traversing back up the tree when doing in-order traversal
229
+ self._stack = []
230
+
231
+ @property
232
+ def state(self) -> tuple[Page, int, list[tuple[Page, int]]]:
233
+ """Get the current cursor state."""
234
+ return self._page, self._idx, self._stack[:]
235
+
236
+ @state.setter
237
+ def state(self, value: tuple[Page, int, list[tuple[Page, int]]]) -> None:
238
+ """Set the current cursor state."""
239
+ self._page, self._idx, self._stack = value[0], value[1], value[2][:]
240
+
241
+ def reset(self) -> Self:
242
+ """Reset the cursor to the root of the B+Tree."""
243
+ self._page = self.root
244
+ self._idx = 0
245
+ self._stack = []
246
+
247
+ return self
248
+
249
+ def node(self) -> Node:
250
+ """Return the node the cursor is currently on.
251
+
252
+ Returns:
253
+ A :class:`~dissect.database.ese.page.Node` object of the current node.
254
+ """
255
+ return self._page.node(self._idx)
256
+
257
+ def first(self) -> bool:
258
+ """Move the cursor to the first leaf node in the B+Tree."""
259
+ self.reset()
260
+ while self._page.is_branch and self._page.node_count > 0:
261
+ self.push()
262
+
263
+ return self._page.node_count != 0
264
+
265
+ def last(self) -> bool:
266
+ """Move the cursor to the last leaf node in the B+Tree."""
267
+ self.reset()
268
+ while self._page.is_branch and self._page.node_count > 0:
269
+ self._idx = self._page.node_count - 1
270
+ self.push()
271
+
272
+ self._idx = self._page.node_count - 1
273
+ return self._page.node_count != 0
274
+
275
+ def next(self) -> bool:
276
+ """Move the cursor to the next leaf node."""
277
+ if self._page.is_branch:
278
+ # Treat as if we were at the first node
279
+ self.first()
280
+ return self._page.node_count != 0
281
+
282
+ if self._idx + 1 < self._page.node_count:
283
+ self._idx += 1
284
+ elif self._stack:
285
+ # End of current page, traverse to the next leaf page
286
+
287
+ # First pop until we find a page with unvisited nodes
288
+ while self._idx + 1 >= self._page.node_count:
289
+ if not self._stack:
290
+ return False
291
+ self.pop()
292
+
293
+ self._idx += 1
294
+
295
+ # Then push down to the next page
296
+ while self._page.is_branch:
297
+ self.push()
298
+ else:
299
+ return False
300
+
301
+ return True
302
+
303
+ def prev(self) -> bool:
304
+ """Move the cursor to the previous leaf node."""
305
+ if self._page.is_branch:
306
+ # Treat as if we were at the last node
307
+ self.last()
308
+ return self._page.node_count != 0
309
+
310
+ if self._idx - 1 >= 0:
311
+ self._idx -= 1
312
+ elif self._stack:
313
+ # Start of current page, traverse to the previous leaf page
314
+
315
+ # First pop until we find a page with unvisited nodes
316
+ while self._idx - 1 < 0:
317
+ if not self._stack:
318
+ # Start of B+Tree reached
319
+ return False
320
+ self.pop()
321
+
322
+ self._idx -= 1
323
+
324
+ # Then push down to the rightmost leaf
325
+ while self._page.is_branch:
326
+ self._idx = self._page.node_count - 1
327
+ self.push()
328
+ else:
329
+ # Start of B+Tree reached
330
+ return False
331
+
332
+ return True
333
+
334
+ def push(self) -> Self:
335
+ """Push down to the child page at the current index."""
336
+ child_page = self.db.page(self._page.node(self._idx).child)
337
+
338
+ self._stack.append((self._page, self._idx))
339
+ self._page = child_page
340
+ self._idx = 0
341
+
342
+ return self
343
+
344
+ def pop(self) -> Self:
345
+ """Pop back to the parent page."""
346
+ if not self._stack:
347
+ raise IndexError("Cannot pop from an empty stack")
348
+
349
+ self._page, self._idx = self._stack.pop()
350
+
351
+ return self
352
+
353
+ def walk(self) -> Iterator[Node]:
354
+ """Walk the B+Tree in order, yielding nodes."""
355
+ if self.first():
356
+ yield self.node()
357
+
358
+ while self.next():
359
+ yield self.node()
360
+
361
+ def search(self, key: bytes, *, exact: bool = True) -> Self:
362
+ """Search the tree for the given ``key``.
363
+
364
+ Moves the cursor to the matching node, or on the last node that is less than the requested key.
365
+
366
+ Args:
367
+ key: The key to search for.
368
+ exact: Whether to only return successfully on an exact match.
369
+
370
+ Raises:
371
+ KeyNotFoundError: If an ``exact`` match was requested but not found.
372
+ """
373
+ self.reset()
374
+
375
+ while self._page.is_branch:
376
+ self._idx = find_node(self._page, key, exact=False)
377
+ self.push()
378
+
379
+ self._idx = find_node(self._page, key, exact=exact)
380
+ if self._idx >= self._page.node_count or self._idx == -1:
381
+ raise KeyNotFoundError(f"Key not found: {key!r}")
382
+
383
+ return self
384
+
385
+
386
+ def find_node(page: Page, key: bytes, *, exact: bool) -> int:
387
+ """Search a page for a node matching the given key.
388
+
389
+ Referencing Extensible-Storage-Engine source, they bail out early if they find an exact match.
390
+ However, we prefer to always find the _first_ node that is greater than or equal to the key,
391
+ so we can handle cases where there are duplicate index keys. This is important for "range" searches
392
+ where we want to find all keys matching a certain prefix, and not end up somewhere in the middle of the range.
393
+
394
+ Args:
395
+ page: The page to search.
396
+ key: The key to search.
397
+ exact: Whether to only return successfully on an exact match.
398
+
399
+ Returns:
400
+ The node number of the first node that's greater than or equal to the key, or the last node on the page if
401
+ the key is larger than all nodes. If ``exact`` is ``True`` and an exact match is not found, returns -1.
402
+ """
403
+ if page.node_count == 0:
404
+ return -1
405
+
406
+ lo, hi = 0, page.node_count - 1
407
+
408
+ node = None
409
+ while lo < hi:
410
+ mid = (lo + hi) // 2
411
+ node = page.node(mid)
412
+
413
+ # It turns out that the way BTree keys are compared matches 1:1 with how Python compares bytes
414
+ # First compare data, then length
415
+ if key > node.key:
416
+ lo = mid + 1
417
+ else:
418
+ hi = mid
419
+
420
+ # Final comparison on the last node
421
+ node = page.node(lo)
422
+
423
+ if key == node.key:
424
+ if page.is_branch:
425
+ # If there's an exact match on a key on a branch page, the actual leaf nodes are in the next branch
426
+ # Page keys for branch pages appear to be non-inclusive upper bounds
427
+ lo = min(lo + 1, page.node_count - 1)
428
+
429
+ # key != node.key
430
+ elif exact:
431
+ return -1
432
+
433
+ return lo
@@ -114,10 +114,17 @@ class DataTable:
114
114
  yield (obj := stack.pop())
115
115
  stack.extend(obj.children())
116
116
 
117
- def iter(self) -> Iterator[Object]:
118
- """Iterate over all objects in the NTDS database."""
117
+ def iter(self, raw: bool = False) -> Iterator[Object]:
118
+ """Iterate over all objects in the NTDS database.
119
+
120
+ Args:
121
+ raw: Whether to return base :class:`Object` instances without upcasting to more specific types
122
+ based on the objectClass.
123
+ """
124
+ from_record = Object if raw else Object.from_record
125
+
119
126
  for record in self.table.records():
120
- yield Object.from_record(self.db, record)
127
+ yield from_record(self.db, record)
121
128
 
122
129
  def get(self, dnt: int) -> Object:
123
130
  """Retrieve an object by its Directory Number Tag (DNT) value.
@@ -1,14 +1,25 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, BinaryIO
4
+ from uuid import UUID
4
5
 
5
6
  from dissect.database.ese.ntds.database import Database
7
+ from dissect.database.ese.ntds.objects.secret import BackupKey
6
8
 
7
9
  if TYPE_CHECKING:
8
10
  from collections.abc import Iterator
9
11
 
10
- from dissect.database.ese.ntds.objects import Computer, DomainDNS, Group, GroupPolicyContainer, Object, Server, User
11
- from dissect.database.ese.ntds.objects.trusteddomain import TrustedDomain
12
+ from dissect.database.ese.ntds.objects import (
13
+ Computer,
14
+ DomainDNS,
15
+ Group,
16
+ GroupPolicyContainer,
17
+ Object,
18
+ Secret,
19
+ Server,
20
+ TrustedDomain,
21
+ User,
22
+ )
12
23
  from dissect.database.ese.ntds.pek import PEK
13
24
 
14
25
 
@@ -93,3 +104,37 @@ class NTDS:
93
104
  def group_policies(self) -> Iterator[GroupPolicyContainer]:
94
105
  """Get all group policy objects (GPO) objects from the database."""
95
106
  yield from self.search(objectClass="groupPolicyContainer")
107
+
108
+ def secrets(self) -> Iterator[Secret]:
109
+ """Get all secret objects from the database."""
110
+ yield from self.search(objectClass="secret")
111
+
112
+ def backup_keys(self) -> Iterator[BackupKey]:
113
+ """Get all DPAPI backup keys from the database."""
114
+ if not self.pek.unlocked:
115
+ raise ValueError("PEK must be unlocked to retrieve backup keys")
116
+
117
+ for secret in self.secrets():
118
+ if secret.is_phantom or not secret.name.startswith("BCKUPKEY_") or secret.name.startswith("BCKUPKEY_P"):
119
+ continue
120
+
121
+ yield BackupKey(secret)
122
+
123
+ def preferred_backup_keys(self) -> Iterator[BackupKey]:
124
+ """Get preferred DPAPI backup keys from the database."""
125
+ if not self.pek.unlocked:
126
+ raise ValueError("PEK must be unlocked to retrieve backup keys")
127
+
128
+ # We could do this the proper way (lookup the BCKUPKEY_P* secrets and then directly lookup the
129
+ # corresponding BCKUPKEY_* secrets), but in practice there are only a few backup keys, so just
130
+ # filter after the fact
131
+ preferred_guids = []
132
+ for secret in self.secrets():
133
+ if secret.is_phantom or not secret.name.startswith("BCKUPKEY_P"):
134
+ continue
135
+
136
+ preferred_guids.append(UUID(bytes_le=secret.current_value))
137
+
138
+ for key in self.backup_keys():
139
+ if key.guid in preferred_guids:
140
+ yield key
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cached_property
4
+ from typing import TYPE_CHECKING
5
+ from uuid import UUID
6
+
7
+ from dissect.util.ts import wintimestamp
8
+
9
+ from dissect.database.ese.ntds.c_ds import c_ds
10
+ from dissect.database.ese.ntds.objects.leaf import Leaf
11
+
12
+ if TYPE_CHECKING:
13
+ from datetime import datetime
14
+
15
+
16
+ class Secret(Leaf):
17
+ """Represents a secret object in the Active Directory.
18
+
19
+ References:
20
+ - https://learn.microsoft.com/en-us/windows/win32/adschema/c-secret
21
+ """
22
+
23
+ __object_class__ = "secret"
24
+
25
+ def __repr_body__(self) -> str:
26
+ return f"name={self.name!r} last_set_time={self.last_set_time} prior_set_time={self.prior_set_time}"
27
+
28
+ @property
29
+ def current_value(self) -> bytes:
30
+ """Return the current value of the secret."""
31
+ return self.get("currentValue")
32
+
33
+ @property
34
+ def last_set_time(self) -> datetime | None:
35
+ """Return the last set time of the secret."""
36
+ if (ts := self.get("lastSetTime")) is not None:
37
+ return wintimestamp(ts)
38
+ return None
39
+
40
+ @property
41
+ def prior_value(self) -> bytes:
42
+ """Return the prior value of the secret."""
43
+ return self.get("priorValue")
44
+
45
+ @property
46
+ def prior_set_time(self) -> datetime | None:
47
+ """Return the prior set time of the secret."""
48
+ if (ts := self.get("priorSetTime")) is not None:
49
+ return wintimestamp(ts)
50
+ return None
51
+
52
+
53
+ class BackupKey:
54
+ """Represents a DPAPI backup key object in the Active Directory."""
55
+
56
+ def __init__(self, secret: Secret):
57
+ self.secret = secret
58
+
59
+ def __repr__(self) -> str:
60
+ return f"<BackupKey guid={self.guid} version={self.version}>"
61
+
62
+ @cached_property
63
+ def guid(self) -> UUID:
64
+ """The GUID of the backup key."""
65
+ return UUID(self.secret.name.removeprefix("BCKUPKEY_").removesuffix(" Secret"))
66
+
67
+ @cached_property
68
+ def version(self) -> int:
69
+ """The version of the backup key."""
70
+ return c_ds.DWORD(self.secret.current_value)
71
+
72
+ @cached_property
73
+ def is_legacy(self) -> bool:
74
+ """Whether the backup key is a legacy key (version 1)."""
75
+ return self.version == 1
76
+
77
+ @cached_property
78
+ def key(self) -> bytes:
79
+ """The key bytes of the backup key, for legacy keys (version 1)."""
80
+ if self.version == 1:
81
+ return self.secret.current_value[4:]
82
+ raise TypeError(f"Backup key version {self.version} does not have a single key value")
83
+
84
+ @cached_property
85
+ def private_key(self) -> bytes:
86
+ """The private key bytes of the backup key, for version 2 keys."""
87
+ if self.version == 2:
88
+ private_length = c_ds.DWORD(self.secret.current_value[4:8])
89
+ return self.secret.current_value[12 : 12 + private_length]
90
+ raise TypeError(f"Backup key version {self.version} does not have a private key value")
91
+
92
+ @cached_property
93
+ def public_key(self) -> bytes:
94
+ """The public key bytes of the backup key, for version 2 keys."""
95
+ if self.version == 2:
96
+ private_length = c_ds.DWORD(self.secret.current_value[4:8])
97
+ public_length = c_ds.DWORD(self.secret.current_value[8:12])
98
+ return self.secret.current_value[12 + private_length : 12 + private_length + public_length]
99
+ raise TypeError(f"Backup key version {self.version} does not have a public key value")