dissect.database 1.2.dev1__tar.gz → 1.2.dev2__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.dev2}/PKG-INFO +1 -1
- dissect_database-1.2.dev2/dissect/database/ese/cursor.py +433 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/database.py +10 -3
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/query.py +56 -59
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/page.py +4 -4
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/table.py +6 -10
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2/dissect.database.egg-info}/PKG-INFO +1 -1
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/SOURCES.txt +0 -1
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/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-1.2.dev2}/COPYRIGHT +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/LICENSE +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/MANIFEST.in +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/README.md +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/c_db.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/c_db.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/db.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/tools/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/tools/c_rpm.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/tools/c_rpm.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/bsd/tools/rpm.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/c_ese.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/c_ese.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/compression.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ese.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/exception.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/index.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/lcmapstring.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_ds.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_ds.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_pek.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_pek.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_sd.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/c_sd.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/ntds.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/applicationsettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/attributeschema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/builtindomain.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/certificationauthority.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/classschema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/classstore.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/computer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/configuration.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/container.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/controlaccessright.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/crldistributionpoint.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/crossref.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/crossrefcontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dfsconfiguration.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/displayspecifier.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dmd.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dnsnode.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dnszone.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/domain.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/domaindns.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/domainpolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/dsuisettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/filelinktracking.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/foreignsecurityprincipal.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/group.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/grouppolicycontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/infrastructureupdate.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/intersitetransport.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/intersitetransportcontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecbase.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecfilter.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecisakmppolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecnegotiationpolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecnfa.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ipsecpolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/leaf.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/linktrackobjectmovetable.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/linktrackvolumetable.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/locality.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/lostandfound.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msauthz_centralaccesspolicies.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msauthz_centralaccessrules.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_content.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_contentset.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_globalsettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_localsettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_member.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_replicationgroup.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_subscriber.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_subscription.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdfsr_topology.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msdns_serversettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_authnpolicies.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_authnpolicysilos.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_claimstransformationpolicies.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_claimtype.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_claimtypepropertybase.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_claimtypes.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_optionalfeature.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_passwordsettingscontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_quotacontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_resourceproperties.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_resourceproperty.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_resourcepropertylist.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_shadowprincipalcontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msds_valuetype.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msimaging_psps.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/mskds_provserverconfiguration.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msmqenterprisesettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/mspki_enterpriseoid.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/mspki_privatekeyrecoveryagent.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/msspp_activationobjectscontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/mstpm_informationobjectscontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntdsconnection.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntdsdsa.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntdsservice.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntdssitesettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ntfrssettings.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/object.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/organizationalperson.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/organizationalunit.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/person.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/physicallocation.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/pkicertificatetemplate.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/pkienrollmentservice.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/querypolicy.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ridmanager.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/ridset.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/rpccontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/rrasadministrationdictionary.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/samserver.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/secret.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/securityobject.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/server.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/serverscontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/site.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/sitelink.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/sitescontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/subnetcontainer.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/subschema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/top.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/trusteddomain.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/objects/user.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/pek.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/schema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/sd.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/tools/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/tools/ntds.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/ntds/util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/record.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/sorting_table.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/certlog.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/impacket.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/sru.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/tools/ual.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/ese/util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/exception.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/c_sqlite3.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/c_sqlite3.pyi +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/encryption/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/encryption/sqlcipher/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/encryption/sqlcipher/exception.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/encryption/sqlcipher/sqlcipher.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/exception.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/sqlite3.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect/database/sqlite3/wal.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/dependency_links.txt +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/entry_points.txt +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/requires.txt +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/dissect.database.egg-info/top_level.txt +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/pyproject.toml +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/setup.cfg +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_docs/Makefile +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_docs/conf.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_docs/index.rst +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_tools/sqlite3/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_tools/sqlite3/generate_sqlite.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/_util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/bsd/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/bsd/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/bsd/test_db.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/bsd/test_rpm.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_benchmark.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_ntds.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_pek.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_schema.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_sd.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/ntds/test_util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_cursor.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_ese.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_index.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_page.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_record.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/test_table.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/tools/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/tools/test_certlog.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/tools/test_sru.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/ese/tools/test_ual.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/__init__.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/conftest.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_default_values.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_row.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_sqlcipher.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_sqlite3.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_util.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/tests/sqlite3/test_wal.py +0 -0
- {dissect_database-1.2.dev1 → dissect_database-1.2.dev2}/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.dev2
|
|
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.dev2}/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,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import fnmatch
|
|
3
4
|
import logging
|
|
4
|
-
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
5
7
|
|
|
6
8
|
from dissect.util.ldap import LogicalOperator, SearchFilter
|
|
7
9
|
|
|
@@ -33,30 +35,25 @@ class Query:
|
|
|
33
35
|
"""
|
|
34
36
|
yield from self._process_query(self._filter)
|
|
35
37
|
|
|
36
|
-
def _process_query(self, filter: SearchFilter, records:
|
|
38
|
+
def _process_query(self, filter: SearchFilter, records: Iterator[Record] | None = None) -> Iterator[Record]:
|
|
37
39
|
"""Process LDAP query recursively, handling nested logical operations.
|
|
38
40
|
|
|
39
41
|
Args:
|
|
40
42
|
filter: The LDAP search filter to process.
|
|
41
|
-
records: Optional
|
|
43
|
+
records: Optional iterable of records to filter instead of querying the database.
|
|
42
44
|
|
|
43
45
|
Yields:
|
|
44
46
|
Records matching the search filter.
|
|
45
47
|
"""
|
|
46
|
-
if
|
|
47
|
-
if
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if filter.operator == LogicalOperator.AND:
|
|
57
|
-
yield from self._process_and_operation(filter, records)
|
|
58
|
-
elif filter.operator == LogicalOperator.OR:
|
|
59
|
-
yield from self._process_or_operation(filter, records)
|
|
48
|
+
if filter.is_nested():
|
|
49
|
+
if filter.operator == LogicalOperator.AND:
|
|
50
|
+
yield from self._process_and_operation(filter, records)
|
|
51
|
+
elif filter.operator == LogicalOperator.OR:
|
|
52
|
+
yield from self._process_or_operation(filter, records)
|
|
53
|
+
elif records is not None:
|
|
54
|
+
yield from self._filter_records(filter, records)
|
|
55
|
+
else:
|
|
56
|
+
yield from self._query_database(filter)
|
|
60
57
|
|
|
61
58
|
def _query_database(self, filter: SearchFilter) -> Iterator[Record]:
|
|
62
59
|
"""Execute a simple LDAP filter against the database.
|
|
@@ -73,25 +70,33 @@ class Query:
|
|
|
73
70
|
|
|
74
71
|
# Get the database index for this attribute
|
|
75
72
|
if (index := self.db.data.table.find_index([schema.column])) is None:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# Handle wildcard searches differently
|
|
80
|
-
if filter.value.endswith("*"):
|
|
81
|
-
yield from _process_wildcard_tail(index, filter.value)
|
|
82
|
-
else:
|
|
83
|
-
raise NotImplementedError("Wildcards in the middle or start of the value are not yet supported")
|
|
73
|
+
# If no index is available, we have to scan the entire table
|
|
74
|
+
log.debug("No index found for attribute %s (%s), scanning entire table", filter.attribute, schema.column)
|
|
75
|
+
yield from self._filter_records(filter, self.db.data.table.records())
|
|
84
76
|
else:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
if "*" in filter.value:
|
|
78
|
+
# Handle wildcard searches differently
|
|
79
|
+
if filter.value.endswith("*"):
|
|
80
|
+
yield from _process_wildcard_tail(index, filter.value)
|
|
81
|
+
else:
|
|
82
|
+
# For more complex wildcard patterns, we need to scan the index and apply the filter
|
|
83
|
+
log.debug(
|
|
84
|
+
"Complex wildcard search for attribute %s (%s), scanning entire index",
|
|
85
|
+
filter.attribute,
|
|
86
|
+
schema.column,
|
|
87
|
+
)
|
|
88
|
+
yield from self._filter_records(filter, index.cursor())
|
|
89
|
+
else:
|
|
90
|
+
# Exact match query
|
|
91
|
+
encoded_value = encode_value(self.db, schema, filter.value)
|
|
92
|
+
yield from index.cursor().find_all(**{schema.column: encoded_value})
|
|
88
93
|
|
|
89
|
-
def _process_and_operation(self, filter: SearchFilter, records:
|
|
94
|
+
def _process_and_operation(self, filter: SearchFilter, records: Iterator[Record] | None) -> Iterator[Record]:
|
|
90
95
|
"""Process AND logical operation.
|
|
91
96
|
|
|
92
97
|
Args:
|
|
93
98
|
filter: The LDAP search filter with AND operator.
|
|
94
|
-
records: Optional
|
|
99
|
+
records: Optional iterable of records to filter.
|
|
95
100
|
|
|
96
101
|
Yields:
|
|
97
102
|
Records matching all conditions in the AND operation.
|
|
@@ -102,19 +107,19 @@ class Query:
|
|
|
102
107
|
else:
|
|
103
108
|
# Use the first child as base query, then filter with remaining children
|
|
104
109
|
base_query, *remaining_children = filter.children
|
|
105
|
-
records_to_process =
|
|
110
|
+
records_to_process = self._process_query(base_query)
|
|
106
111
|
children_to_check = remaining_children
|
|
107
112
|
|
|
108
113
|
for record in records_to_process:
|
|
109
114
|
if all(any(self._process_query(child, records=[record])) for child in children_to_check):
|
|
110
115
|
yield record
|
|
111
116
|
|
|
112
|
-
def _process_or_operation(self, filter: SearchFilter, records:
|
|
117
|
+
def _process_or_operation(self, filter: SearchFilter, records: Iterator[Record] | None) -> Iterator[Record]:
|
|
113
118
|
"""Process OR logical operation.
|
|
114
119
|
|
|
115
120
|
Args:
|
|
116
121
|
filter: The LDAP search filter with OR operator.
|
|
117
|
-
records: Optional
|
|
122
|
+
records: Optional iterable of records to filter.
|
|
118
123
|
|
|
119
124
|
Yields:
|
|
120
125
|
Records matching any condition in the OR operation.
|
|
@@ -122,12 +127,12 @@ class Query:
|
|
|
122
127
|
for child in filter.children:
|
|
123
128
|
yield from self._process_query(child, records=records)
|
|
124
129
|
|
|
125
|
-
def _filter_records(self, filter: SearchFilter, records:
|
|
126
|
-
"""Filter
|
|
130
|
+
def _filter_records(self, filter: SearchFilter, records: Iterator[Record]) -> Iterator[Record]:
|
|
131
|
+
"""Filter an iterable of records against a simple LDAP filter.
|
|
127
132
|
|
|
128
133
|
Args:
|
|
129
134
|
filter: The LDAP search filter to apply.
|
|
130
|
-
records: The
|
|
135
|
+
records: The iterable of records to filter.
|
|
131
136
|
|
|
132
137
|
Yields:
|
|
133
138
|
Records that match the filter criteria.
|
|
@@ -136,14 +141,26 @@ class Query:
|
|
|
136
141
|
return
|
|
137
142
|
|
|
138
143
|
encoded_value = encode_value(self.db, schema, filter.value)
|
|
144
|
+
re_encoded_value = None
|
|
139
145
|
|
|
140
|
-
has_wildcard = "*" in filter.value
|
|
141
|
-
|
|
146
|
+
has_wildcard = "*" in filter.value and isinstance(encoded_value, str)
|
|
147
|
+
if has_wildcard:
|
|
148
|
+
re_encoded_value = re.compile(fnmatch.translate(encoded_value), re.IGNORECASE)
|
|
142
149
|
|
|
143
150
|
for record in records:
|
|
144
151
|
record_value = record.get(schema.column)
|
|
145
152
|
|
|
146
|
-
if
|
|
153
|
+
if isinstance(record_value, list):
|
|
154
|
+
# Currently assume that we can only search for single values, not lists
|
|
155
|
+
if has_wildcard and record_value and isinstance(record_value[0], str):
|
|
156
|
+
if any(re_encoded_value.match(rv) for rv in record_value):
|
|
157
|
+
yield record
|
|
158
|
+
elif encoded_value in record_value:
|
|
159
|
+
yield record
|
|
160
|
+
|
|
161
|
+
elif (
|
|
162
|
+
has_wildcard and isinstance(record_value, str) and re_encoded_value.match(record_value)
|
|
163
|
+
) or record_value == encoded_value:
|
|
147
164
|
yield record
|
|
148
165
|
|
|
149
166
|
|
|
@@ -174,26 +191,6 @@ def _process_wildcard_tail(index: Index, filter_value: str) -> Iterator[Record]:
|
|
|
174
191
|
record = cursor.next()
|
|
175
192
|
|
|
176
193
|
|
|
177
|
-
def _value_matches_filter(
|
|
178
|
-
record_value: Any, encoded_value: Any, has_wildcard: bool, wildcard_prefix: str | None
|
|
179
|
-
) -> bool:
|
|
180
|
-
"""Return whether a record value matches the filter criteria.
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
record_value: The value from the database record.
|
|
184
|
-
encoded_value: The encoded filter value to match against.
|
|
185
|
-
has_wildcard: Whether the filter contains wildcard characters.
|
|
186
|
-
wildcard_prefix: The prefix to match for wildcard searches.
|
|
187
|
-
"""
|
|
188
|
-
if isinstance(record_value, list):
|
|
189
|
-
return encoded_value in record_value
|
|
190
|
-
|
|
191
|
-
if has_wildcard and wildcard_prefix and isinstance(record_value, str):
|
|
192
|
-
return record_value.lower().startswith(wildcard_prefix)
|
|
193
|
-
|
|
194
|
-
return encoded_value == record_value
|
|
195
|
-
|
|
196
|
-
|
|
197
194
|
def _increment_last_char(value: str) -> str:
|
|
198
195
|
"""Increment the last character in a string to find the next lexicographically sortable key.
|
|
199
196
|
|
|
@@ -49,6 +49,9 @@ class Page:
|
|
|
49
49
|
self._node_cls = LeafNode if self.is_leaf else BranchNode
|
|
50
50
|
self._node_cache = {}
|
|
51
51
|
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return f"<Page num={self.num:d} flags={self.flags.name} nodes={self.node_count}>"
|
|
54
|
+
|
|
52
55
|
@cached_property
|
|
53
56
|
def is_small_page(self) -> bool:
|
|
54
57
|
return self.db.has_small_pages
|
|
@@ -123,7 +126,7 @@ class Page:
|
|
|
123
126
|
IndexError: If the node number is out of bounds.
|
|
124
127
|
"""
|
|
125
128
|
if num < 0 or num > self.node_count - 1:
|
|
126
|
-
raise IndexError(f"Node number exceeds boundaries
|
|
129
|
+
raise IndexError(f"Node number exceeds boundaries 0-{self.node_count - 1}: {num}")
|
|
127
130
|
|
|
128
131
|
if num not in self._node_cache:
|
|
129
132
|
self._node_cache[num] = self._node_cls(self.tag(num + 1))
|
|
@@ -161,9 +164,6 @@ class Page:
|
|
|
161
164
|
if self.is_root and leaf and leaf.tag.page.next_page:
|
|
162
165
|
yield from db.page(leaf.tag.page.next_page).iter_leaf_nodes()
|
|
163
166
|
|
|
164
|
-
def __repr__(self) -> str:
|
|
165
|
-
return f"<Page num={self.num:d}>"
|
|
166
|
-
|
|
167
167
|
|
|
168
168
|
class Tag:
|
|
169
169
|
"""A tag is the "physical" data entry of a page.
|