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.
- {dissect_database-1.2.dev1/dissect.database.egg-info → dissect_database-1.2.dev3}/PKG-INFO +1 -1
- dissect_database-1.2.dev3/dissect/database/ese/cursor.py +433 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/database.py +10 -3
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/ntds.py +47 -2
- dissect_database-1.2.dev3/dissect/database/ese/ntds/objects/secret.py +99 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/query.py +56 -59
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/page.py +4 -4
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/table.py +6 -10
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3/dissect.database.egg-info}/PKG-INFO +1 -1
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/SOURCES.txt +0 -1
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_ntds.py +28 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_query.py +19 -8
- dissect_database-1.2.dev1/dissect/database/ese/btree.py +0 -177
- dissect_database-1.2.dev1/dissect/database/ese/cursor.py +0 -230
- dissect_database-1.2.dev1/dissect/database/ese/ntds/objects/secret.py +0 -13
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/COPYRIGHT +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/LICENSE +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/MANIFEST.in +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/README.md +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/c_db.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/c_db.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/db.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/tools/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/tools/c_rpm.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/tools/c_rpm.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/bsd/tools/rpm.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/c_ese.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/c_ese.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/compression.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ese.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/exception.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/index.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/lcmapstring.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_ds.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_ds.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_pek.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_pek.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_sd.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/c_sd.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/applicationsettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/attributeschema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/builtindomain.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/certificationauthority.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/classschema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/classstore.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/computer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/configuration.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/container.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/controlaccessright.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/crldistributionpoint.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/crossref.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/crossrefcontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dfsconfiguration.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/displayspecifier.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dmd.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dnsnode.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dnszone.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/domain.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/domaindns.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/domainpolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/dsuisettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/filelinktracking.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/foreignsecurityprincipal.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/group.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/grouppolicycontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/infrastructureupdate.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/intersitetransport.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/intersitetransportcontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecbase.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecfilter.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecisakmppolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecnegotiationpolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecnfa.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ipsecpolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/leaf.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/linktrackobjectmovetable.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/linktrackvolumetable.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/locality.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/lostandfound.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msauthz_centralaccesspolicies.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msauthz_centralaccessrules.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_content.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_contentset.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_globalsettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_localsettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_member.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_replicationgroup.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_subscriber.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_subscription.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdfsr_topology.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msdns_serversettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_authnpolicies.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_authnpolicysilos.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_claimstransformationpolicies.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_claimtype.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_claimtypepropertybase.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_claimtypes.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_optionalfeature.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_passwordsettingscontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_quotacontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_resourceproperties.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_resourceproperty.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_resourcepropertylist.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_shadowprincipalcontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msds_valuetype.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msimaging_psps.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/mskds_provserverconfiguration.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msmqenterprisesettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/mspki_enterpriseoid.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/mspki_privatekeyrecoveryagent.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/msspp_activationobjectscontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/mstpm_informationobjectscontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntdsconnection.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntdsdsa.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntdsservice.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntdssitesettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ntfrssettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/object.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/organizationalperson.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/organizationalunit.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/person.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/physicallocation.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/pkicertificatetemplate.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/pkienrollmentservice.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/querypolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ridmanager.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/ridset.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/rpccontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/rrasadministrationdictionary.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/samserver.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/securityobject.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/server.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/serverscontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/site.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/sitelink.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/sitescontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/subnetcontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/subschema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/top.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/trusteddomain.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/objects/user.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/pek.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/schema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/sd.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/tools/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/tools/ntds.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/record.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/sorting_table.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/certlog.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/impacket.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/sru.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/tools/ual.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/exception.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/c_sqlite3.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/c_sqlite3.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/encryption/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/encryption/sqlcipher/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/encryption/sqlcipher/exception.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/encryption/sqlcipher/sqlcipher.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/exception.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/sqlite3.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/sqlite3/wal.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/dependency_links.txt +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/entry_points.txt +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/requires.txt +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect.database.egg-info/top_level.txt +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/pyproject.toml +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/setup.cfg +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_docs/Makefile +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_docs/conf.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_docs/index.rst +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_tools/sqlite3/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_tools/sqlite3/generate_sqlite.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/_util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/bsd/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/bsd/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/bsd/test_db.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/bsd/test_rpm.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_benchmark.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_pek.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_schema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_sd.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/ntds/test_util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_cursor.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_ese.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_index.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_page.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_record.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/test_table.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/tools/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/tools/test_certlog.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/tools/test_sru.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/ese/tools/test_ual.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_default_values.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_row.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_sqlcipher.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_sqlite3.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/tests/sqlite3/test_wal.py +0 -0
- {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.
|
|
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
|
{dissect_database-1.2.dev1 → dissect_database-1.2.dev3}/dissect/database/ese/ntds/database.py
RENAMED
|
@@ -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
|
|
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
|
|
11
|
-
|
|
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")
|