Karthik Pattabiraman

204 papers A* 19A 61B 5C 8Misc 16Journal 52Unranked 41
YearRankTypeTitle / Venue / Authors
2026 J jnl
CoRR
Karthik Pattabiraman, Mihir Patel, Fred Lin
2026 J jnl
IEEE Trans. Dependable Secur. Comput.
Pritam Dash, Guanpeng Li, Zitao Chen, Mehdi Karimibiuki, Karthik Pattabiraman
2026 J jnl
CoRR
Mohammed Elnawawy, Gargi Mitra, Shahrear Iqbal, Karthik Pattabiraman
2026 J jnl
CoRR
Mohsen Salehi, Karthik Pattabiraman
2026 A conf
EuroSys
Kumseok Jung, Mohanna Shahrad, Gargi Mitra, Karthik Pattabiraman
2025 A* conf
NDSS
Zitao Chen, Karthik Pattabiraman
2025 J jnl
CoRR
Pritam Dash, Ethan Chan, Nathan P. Lawrence, Karthik Pattabiraman
2025 A* conf
CCS
Zitao Chen, Karthik Pattabiraman
2025 J jnl
ACM Trans. Embed. Comput. Syst.
Mohammad Rafiuzzaman, Sathish Gopalakrishnan, Karthik Pattabiraman
2025 Misc conf
SAC
Abraham Chan, Arpan Gujarati, Karthik Pattabiraman, Sathish Gopalakrishnan
2025 A conf
ISSRE
SeyedMani Sadati, Abraham Chan, Udit Kumar Agarwal, Karthik Pattabiraman
2025 conf
DSN-S
Xun Jiao, Abhinav Pandey, Karthik Pattabiraman, Fan Fred Lin
2025 conf
DSN-W
Mohammed Elnawawy, Gargi Mitra, Shahrear Iqbal, Karthik Pattabiraman
2025 J jnl
CoRR
Mohammed Elnawawy, Gargi Mitra, Shahrear Iqbal, Karthik Pattabiraman
2025 J jnl
IEEE Trans. Parallel Distributed Syst.
Kumseok Jung, Julien Gascon-Samson, Sathish Gopalakrishnan, Karthik Pattabiraman
2025 A conf
DSN
Pritam Dash, Karthik Pattabiraman
2025 A conf
DSN
Abraham Chan, Arpan Gujarati, Karthik Pattabiraman, Sathish Gopalakrishnan
2025 A conf
DSN
Yuqi Liu, Rui Xi, Karthik Pattabiraman
2025 J jnl
CoRR
Gargi Mitra, Mohammadreza Hallajiyan, Inji Kim, Athish Pranav Dharmalingam, Mohammed Elnawawy, Shahrear Iqbal, Karthik Pattabiraman, Homa Alemzadeh
2025 J jnl
IEEE Des. Test
Guanpeng Li, Siva Kumar Sastry Hari, Michael B. Sullivan, Timothy Tsai, Karthik Pattabiraman, Joel S. Emer, Stephen W. Keckler
2024 J jnl
CoRR
Zitao Chen, Karthik Pattabiraman
2024 A* conf
CCS
Mohsen Salehi, Karthik Pattabiraman
2024 J jnl
CoRR
Mohsen Salehi, Karthik Pattabiraman
2024 J jnl
CoRR
Zitao Chen, Karthik Pattabiraman
2024 J jnl
ACM Trans. Cyber Phys. Syst.
Deval Shah, Zi Yu Xue, Karthik Pattabiraman, Tor M. Aamodt
2024 conf
IoTDI
Kumseok Jung, Gargi Mitra, Sathish Gopalakrishnan, Karthik Pattabiraman
2024 A conf
AsiaCCS
Pritam Dash, Guanpeng Li, Mehdi Karimibiuki, Karthik Pattabiraman
2024 conf
AISafety@IJCAI
Syed Sha Qutub, Michael Paulitsch, Karthik Pattabiraman, Korbinian Hagn, Fabian Oboril, Cornelius Bürkle, Kay-Ulrich Scholl, Gereon Hinz, Alois Knoll
2024 J jnl
CoRR
Syed Sha Qutub, Michael Paulitsch, Karthik Pattabiraman, Korbinian Hagn, Fabian Oboril, Cornelius Bürkle, Kay-Ulrich Scholl, Gereon Hinz, Alois Knoll
2024 conf
DSN-S
Abraham Chan, Arpan Gujarati, Karthik Pattabiraman, Sathish Gopalakrishnan
2024 conf
IoTDI
Kumseok Jung, Gargi Mitra, Sathish Gopalakrishnan, Karthik Pattabiraman
2024 A* conf
NDSS
Zitao Chen, Karthik Pattabiraman
2024 A* conf
SP
Rui Xi, Zehua Wang, Karthik Pattabiraman
2024 conf
HealthSec@CCS
Mohammadreza Hallajiyan, Athish Pranav Dharmalingam, Gargi Mitra, Homa Alemzadeh, Shahrear Iqbal, Karthik Pattabiraman
2024 A* conf
CCS
Pritam Dash, Ethan Chan, Karthik Pattabiraman
2024 J jnl
CoRR
Pritam Dash, Ethan Chan, Karthik Pattabiraman
2024 B conf
CHASE
Mohammed Elnawawy, Mohammadreza Hallajiyan, Gargi Mitra, Shahrear Iqbal, Karthik Pattabiraman
2024 J jnl
CoRR
Mohammed Elnawawy, Mohammadreza Hallajiyan, Gargi Mitra, Shahrear Iqbal, Karthik Pattabiraman
2024 conf
DSN-S
Karthik Pattabiraman, Abraham Chan
2023 B conf
SAFECOMP
Florian Geissler, Syed Sha Qutub, Michael Paulitsch, Karthik Pattabiraman
2023 J jnl
CoRR
Florian Geissler, Syed Sha Qutub, Michael Paulitsch, Karthik Pattabiraman
2023 J jnl
Softw. Pract. Exp.
Rui Xi, Karthik Pattabiraman
2023 A* conf
ICSE
Asem Ghaleb, Julia Rubin, Karthik Pattabiraman
2023 conf
DSN-S
Pritam Dash, Karthik Pattabiraman
2023 Misc conf
SEC
Amirhossein Ahmadi, Hazem A. Abdelhafez, Karthik Pattabiraman, Matei Ripeanu
2023 J jnl
CoRR
Stefan Winter, Abraham Chan, Habib Saissi, Karthik Pattabiraman, Neeraj Suri
2023 A conf
ISSRE
Abraham Chan, Arpan Gujarati, Karthik Pattabiraman, Sathish Gopalakrishnan
2023 J jnl
IEEE Trans. Dependable Secur. Comput.
Niranjhana Narayanan, Zitao Chen, Bo Fang, Guanpeng Li, Karthik Pattabiraman, Nathan DeBardeleben
2023 conf
EdgeSys@EuroSys
Amirhossein Ahmadi, Hazem A. Abdelhafez, Shashwat Jaiswal, Karthik Pattabiraman, Matei Ripeanu
2023 J jnl
CoRR
Gargi Mitra, Pritam Dash, Yingao Elaine Yao, Aastha Mehta, Karthik Pattabiraman
2023 A conf
AsiaCCS
Zitao Chen, Pritam Dash, Karthik Pattabiraman
2023 J jnl
J. Parallel Distributed Comput.
Alessio Netti, Yang Peng, Patrik Omland, Michael Paulitsch, Jorge Parra, Gustavo Espinosa, Udit Kumar Agarwal, Abraham Chan, Karthik Pattabiraman
2023 J jnl
CoRR
Zitao Chen, Karthik Pattabiraman
2023 A conf
ISSRE
Udit Kumar Agarwal, Abraham Chan, Karthik Pattabiraman
2023 A conf
SC
Ali Asgari Khoshouyeh, Florian Geissler, Syed Sha Qutub, Michael Paulitsch, Prashant J. Nair, Karthik Pattabiraman
2023 A conf
DSN
Yingao Elaine Yao, Pritam Dash, Karthik Pattabiraman
2023 conf
DSN-S
Udit Kumar Agarwal, Abraham Chan, Ali Asgari Khoshouyeh, Karthik Pattabiraman
2022 conf
IoTDI
Mohammad Rafiuzzaman, Sathish Gopalakrishnan, Karthik Pattabiraman
2022 J jnl
IEEE Trans. Dependable Secur. Comput.
Behrooz Sangchoolie, Karthik Pattabiraman, Johan Karlsson
2022 Misc conf
SEC
Hazem A. Abdelhafez, Hassan Halawa, Amr Almoallim, Amirhossein Ahmadi, Karthik Pattabiraman, Matei Ripeanu
2022 J jnl
IEEE Trans. Dependable Secur. Comput.
Lucas Palazzi, Guanpeng Li, Bo Fang, Karthik Pattabiraman
2022 A conf
ISSRE
Udit Kumar Agarwal, Abraham Chan, Karthik Pattabiraman
2022 A* conf
CCS
Mohsen Salehi, Karthik Pattabiraman
2022 Misc conf
SEC
Kumseok Jung, Julien Gascon-Samson, Karthik Pattabiraman
2022 A* conf
CCS
Yingao Elaine Yao, Pritam Dash, Karthik Pattabiraman
2022 J jnl
CoRR
Pritam Dash, Guanpeng Li, Mehdi Karimibiuki, Karthik Pattabiraman
2022 A conf
DSN
Abraham Chan, Arpan Gujarati, Karthik Pattabiraman, Sathish Gopalakrishnan
2022 conf
DSN Workshops
Abraham Chan, Arpan Gujarati, Karthik Pattabiraman, Sathish Gopalakrishnan
2022 A conf
SANER
Rui Xi, Karthik Pattabiraman
2022 A conf
ISSTA
Asem Ghaleb, Julia Rubin, Karthik Pattabiraman
2021 conf
ISSRE Workshops
Abraham Chan, Udit Kumar Agarwal, Karthik Pattabiraman
2021 A conf
DSN
Zitao Chen, Guanpeng Li, Karthik Pattabiraman
2021 C conf
PRDC
Mehdi Karimibiuki, Karthik Pattabiraman, André Ivanov
2021 J jnl
CoRR
Deval Shah, Zi Yu Xue, Karthik Pattabiraman, Tor M. Aamodt
2021 Misc conf
SEC
Kumseok Jung, Julien Gascon-Samson, Karthik Pattabiraman
2021 conf
DSN Workshops
Hui Xu, Guanpeng Li, Homa Alemzadeh, Rakesh Bobba, Varun Chandrasekaran, David E. Evans, Nicolas Papernot, Karthik Pattabiraman, Florian Tramèr
2021 Misc conf
SEC
Hassan Halawa, Hazem A. Abdelhafez, Mohamed Osama Ahmed, Karthik Pattabiraman, Matei Ripeanu
2021 Misc conf
SEC
Kumseok Jung, Julien Gascon-Samson, Karthik Pattabiraman
2021 A conf
DSN
Pritam Dash, Guanpeng Li, Zitao Chen, Mehdi Karimibiuki, Karthik Pattabiraman
2021 J jnl
CoRR
Aarti Kashyap, Syed Mubashir Iqbal, Karthik Pattabiraman, Margo I. Seltzer
2021 conf
EdgeSys@EuroSys
Hazem A. Abdelhafez, Hassan Halawa, Karthik Pattabiraman, Matei Ripeanu
2021 J jnl
DTRAP
Pritam Dash, Mehdi Karimibiuki, Karthik Pattabiraman
2021 conf
DeepTest@ICSE
Niranjhana Narayanan, Karthik Pattabiraman
2021 J jnl
Softw. Pract. Exp.
Kumseok Jung, Julien Gascon-Samson, Shivanshu Goyal, Armin Rezaiean-Asel, Karthik Pattabiraman
2021 conf
AISafety@IJCAI
Florian Geissler, Syed Sha Qutub, Sayanta Roychowdhury, Ali Asgari Khoshouyeh, Yang Peng, Akash Dhamasia, Ralf Graefe, Karthik Pattabiraman, Michael Paulitsch
2021 J jnl
CoRR
Florian Geissler, Syed Sha Qutub, Sayanta Roychowdhury, Ali Asgari Khoshouyeh, Yang Peng, Akash Dhamasia, Ralf Graefe, Karthik Pattabiraman, Michael Paulitsch
2021 J jnl
CoRR
Zitao Chen, Pritam Dash, Karthik Pattabiraman
2021 C conf
QRS
Abraham Chan, Niranjhana Narayanan, Arpan Gujarati, Karthik Pattabiraman, Sathish Gopalakrishnan
2020 C conf
IOLTS
Karthik Pattabiraman, Guanpeng Li, Zitao Chen
2020 A conf
SC
Abdul Rehman Anwer, Guanpeng Li, Karthik Pattabiraman, Michael B. Sullivan, Timothy Tsai, Siva Kumar Sastry Hari
2020 J jnl
CoRR
Asem Ghaleb, Karthik Pattabiraman
2020 A conf
ISSRE
Yong Yang, Yifan Wu, Karthik Pattabiraman, Long Wang, Ying Li
2020 A conf
ISSTA
Asem Ghaleb, Karthik Pattabiraman
2020 conf
ISSRE Workshops
Arpan Gujarati, Sathish Gopalakrishnan, Karthik Pattabiraman
2020 J jnl
CoRR
Zitao Chen, Guanpeng Li, Karthik Pattabiraman
2020 A conf
ISSRE
Zitao Chen, Niranjhana Narayanan, Bo Fang, Guanpeng Li, Karthik Pattabiraman, Nathan DeBardeleben
2020 J jnl
CoRR
Zitao Chen, Niranjhana Narayanan, Bo Fang, Guanpeng Li, Karthik Pattabiraman, Nathan DeBardeleben
2020 conf
DSN Workshops
Homa Alemzadeh, Rakesh Bobba, Varun Chandrasekaran, David E. Evans, Nicolas Papernot, Karthik Pattabiraman, Florian Tramèr
2020 A conf
DSN
Habib Saissi, Stefan Winter, Oliver Schwahn, Karthik Pattabiraman, Neeraj Suri
2019 A conf
ISSRE
Lucas Palazzi, Guanpeng Li, Bo Fang, Karthik Pattabiraman
2019 A conf
ICS
Bo Fang, Hassan Halawa, Karthik Pattabiraman, Matei Ripeanu, Sriram Krishnamoorthy
2019 J jnl
ACM Trans. Embed. Comput. Syst.
Farid Molazem Tabrizi, Karthik Pattabiraman
2019 Misc conf
SAC
Mohammad Rafiuzzaman, Julien Gascon-Samson, Karthik Pattabiraman, Sathish Gopalakrishnan
2019 conf
HotEdge
Kumseok Jung, Julien Gascon-Samson, Karthik Pattabiraman
2019 A conf
ACSAC
Pritam Dash, Mehdi Karimibiuki, Karthik Pattabiraman
2019 conf
DSN (Supplements)
Bo Fang, Jieyang Chen, Karthik Pattabiraman, Matei Ripeanu, Sriram Krishnamoorthy
2018 conf
CPS-SPC@CCS
Ekta Aggarwal, Mehdi Karimibiuki, Karthik Pattabiraman, André Ivanov
2018 conf
DSN Workshops
Homa Alemzadeh, Karthik Pattabiraman, David E. Evans
2018 Misc conf
SEC
Kumseok Jung, Julien Gascon-Samson, Karthik Pattabiraman
2018 C conf
PRDC
Mehdi Karimibiuki, Ekta Aggarwal, Karthik Pattabiraman, André Ivanov
2018 J jnl
IEEE Trans. Dependable Secur. Comput.
Domenico Cotroneo, Karthik Pattabiraman, Antonio Pecchia
2018 A* conf
ICSE
Saba Alimadadi, Ali Mesbah, Karthik Pattabiraman
2018 A conf
DSN
Guanpeng Li, Karthik Pattabiraman
2018 A conf
DSN
Guanpeng Li, Karthik Pattabiraman, Siva Kumar Sastry Hari, Michael B. Sullivan, Timothy Tsai
2018 Misc conf
SEC
Julien Gascon-Samson, Kumseok Jung, Karthik Pattabiraman
2018 conf
ISSRE Workshops
Guanpeng Li, Karthik Pattabiraman, Nathan DeBardeleben
2018 A conf
ECOOP
Julien Gascon-Samson, Kumseok Jung, Shivanshu Goyal, Armin Rezaiean-Asel, Karthik Pattabiraman
2017 J jnl
IEEE Trans. Software Eng.
Frolin S. Ocariza Jr., Kartik Bajaj, Karthik Pattabiraman, Ali Mesbah
2017 conf
ESEC/SIGSOFT FSE
Maryam Raiyat Aliabadi, Amita Ajith Kamath, Julien Gascon-Samson, Karthik Pattabiraman
2017 J jnl
ACM Trans. Embed. Comput. Syst.
Qining Lu, Guanpeng Li, Karthik Pattabiraman, Meeta Sharma Gupta, Jude A. Rivers
2017 A* conf
ASE
Frolin S. Ocariza Jr., Karthik Pattabiraman, Ali Mesbah
2017 A conf
ICST
Abraham Chan, Stefan Winter, Habib Saissi, Karthik Pattabiraman, Neeraj Suri
2017 A conf
HPDC
Bo Fang, Qiang Guan, Nathan DeBardeleben, Karthik Pattabiraman, Matei Ripeanu
2017 A conf
DSN
Behrooz Sangchoolie, Karthik Pattabiraman, Johan Karlsson
2017 conf
SPLASH (Companion Volume)
Julien Gascon-Samson, Mohammad Rafiuzzaman, Karthik Pattabiraman
2017 conf
M4IoT@Middleware
Julien Gascon-Samson, Mohammad Rafiuzzaman, Karthik Pattabiraman
2017 A conf
SC
Guanpeng Li, Siva Kumar Sastry Hari, Michael B. Sullivan, Timothy Tsai, Karthik Pattabiraman, Joel S. Emer, Stephen W. Keckler
2016 J jnl
IEEE Trans. Parallel Distributed Syst.
Bo Fang, Karthik Pattabiraman, Matei Ripeanu, Sudhanva Gurumurthi
2016 A conf
ICST
Shabnam Mirshokraie, Ali Mesbah, Karthik Pattabiraman
2016 J jnl
Softw. Test. Verification Reliab.
Frolin S. Ocariza Jr., Guanpeng Li, Karthik Pattabiraman, Ali Mesbah
2016 J jnl
ACM Trans. Embed. Comput. Syst.
Anna Thomas, Karthik Pattabiraman
2016 B conf
SAFECOMP
Maryam Raiyat Aliabadi, Karthik Pattabiraman
2016 Misc conf
EDCC
Nithya Narayanamurthy, Karthik Pattabiraman, Matei Ripeanu
2016 A conf
ACSAC
Farid Molazem Tabrizi, Karthik Pattabiraman
2016 conf
DSN Workshops
Bo Fang, Panruo Wu, Qiang Guan, Nathan DeBardeleben, Laura Monroe, Sean Blanchard, Zhizong Chen, Karthik Pattabiraman, Matei Ripeanu
2016 J jnl
ACM Trans. Softw. Eng. Methodol.
Saba Alimadadi, Sheldon Sequeira, Ali Mesbah, Karthik Pattabiraman
2016 A* conf
ICSE
Saba Alimadadi, Ali Mesbah, Karthik Pattabiraman
2016 A conf
SC
Guanpeng Li, Karthik Pattabiraman, Chen-Yong Cher, Pradip Bose
2016 A conf
DSN
Bo Fang, Qining Lu, Karthik Pattabiraman, Matei Ripeanu, Sudhanva Gurumurthi
2015 J jnl
IEEE Trans. Reliab.
Layali Rashid, Karthik Pattabiraman, Sathish Gopalakrishnan
2015 conf
ICSE (1)
Frolin S. Ocariza Jr., Karthik Pattabiraman, Ali Mesbah
2015 A conf
ISSRE
Guanpeng Li, Karthik Pattabiraman, Chen-Yong Cher, Pradip Bose
2015 A conf
DSN
Guanpeng Li, Qining Lu, Karthik Pattabiraman
2015 Misc conf
EDCC
Farid Molazem Tabrizi, Karthik Pattabiraman
2015 J jnl
IEEE Trans. Software Eng.
Shabnam Mirshokraie, Ali Mesbah, Karthik Pattabiraman
2015 A conf
ECOOP
Saba Alimadadi, Ali Mesbah, Karthik Pattabiraman
2015 conf
Middleware Doctoral Symposium
Farid Molazem Tabrizi, Karthik Pattabiraman
2015 A conf
ICST
Shabnam Mirshokraie, Ali Mesbah, Karthik Pattabiraman
2015 A* conf
ASE
Kartik Bajaj, Karthik Pattabiraman, Ali Mesbah
2015 C conf
QRS
Qining Lu, Mostafa Farahani, Jiesheng Wei, Anna Thomas, Karthik Pattabiraman
2015 ed.
NOCS
André Ivanov, Diana Marculescu, Partha Pratim Pande, José Flich, Karthik Pattabiraman
2015 A* conf
ASE
Kartik Bajaj, Karthik Pattabiraman, Ali Mesbah
2014 conf
HASE
Farid Molazem Tabrizi, Karthik Pattabiraman
2014 A* conf
ASE
Kartik Bajaj, Karthik Pattabiraman, Ali Mesbah
2014 A conf
DSN
Bo Fang, Karthik Pattabiraman, Matei Ripeanu, Sudhanva Gurumurthi
2014 A conf
ISSRE
Xin Chen, Charng-Da Lu, Karthik Pattabiraman
2014 conf
ISSRE Workshops
Xin Chen, Charng-Da Lu, Karthik Pattabiraman
2014 A conf
DATE
Leonardo Arturo Bautista-Gomez, Franck Cappello, Luigi Carro, Nathan DeBardeleben, Bo Fang, Sudhanva Gurumurthi, Karthik Pattabiraman, Paolo Rech, Matteo Sonza Reorda
2014 B conf
ISPASS
Bo Fang, Karthik Pattabiraman, Matei Ripeanu, Sudhanva Gurumurthi
2014 A conf
DSN
Majid Dadashi, Layali Rashid, Karthik Pattabiraman, Sathish Gopalakrishnan
2014 A conf
MSR
Kartik Bajaj, Karthik Pattabiraman, Ali Mesbah
2014 J jnl
J. Comput. Secur.
Cole Schlesinger, Karthik Pattabiraman, Nikhil Swamy, David Walker, Benjamin G. Zorn
2014 A conf
DSN
Jiesheng Wei, Anna Thomas, Guanpeng Li, Karthik Pattabiraman
2014 Misc conf
CASES
Qining Lu, Karthik Pattabiraman, Meeta Sharma Gupta, Jude A. Rivers
2014 conf
ISSRE Workshops
Maryam Raiyat Aliabadi, Karthik Pattabiraman, Nematollah Bidokhti
2014 A* conf
ICSE
Saba Alimadadi, Sheldon Sequeira, Ali Mesbah, Karthik Pattabiraman
2014 A* conf
ICSE
Frolin S. Ocariza Jr., Karthik Pattabiraman, Ali Mesbah
2013 A conf
ESEM
Frolin S. Ocariza Jr., Kartik Bajaj, Karthik Pattabiraman, Ali Mesbah
2013 A conf
ICST
Shabnam Mirshokraie, Ali Mesbah, Karthik Pattabiraman
2013 A conf
DSN
Anna Thomas, Karthik Pattabiraman
2013 A* conf
ASE
Shabnam Mirshokraie, Ali Mesbah, Karthik Pattabiraman
2013 conf
DSN Workshops
Xin Chen, Charng-Da Lu, Karthik Pattabiraman
2013 J jnl
IEEE Trans. Computers
Karthik Pattabiraman, Nithin Nakka, Zbigniew T. Kalbarczyk, Ravishankar K. Iyer
2012 conf
DSN Workshops
Farid Molazem Tabrizi, Karthik Pattabiraman
2012 conf
SC Companion
Bo Fang, Jiesheng Wei, Karthik Pattabiraman, Matei Ripeanu
2012 A conf
ICST
Frolin S. Ocariza Jr., Karthik Pattabiraman, Ali Mesbah
2012 A conf
DSN
Jiesheng Wei, Karthik Pattabiraman
2012 J jnl
IEEE Trans. Computers
Paruj Ratanaworabhan, Martin Burtscher, Darko Kirovski, Benjamin G. Zorn, Rahul Nagpal, Karthik Pattabiraman
2012 conf
QEST
Layali Rashid, Karthik Pattabiraman, Sathish Gopalakrishnan
2012 conf
SC Companion
Bo Fang, Jiesheng Wei, Karthik Pattabiraman, Matei Ripeanu
2011 J jnl
IEEE Trans. Dependable Secur. Comput.
Karthik Pattabiraman, Zbigniew Kalbarczyk, Ravishankar K. Iyer
2011 J jnl
IEEE Trans. Dependable Secur. Comput.
Karthik Pattabiraman, Giacinto Paolo Saggese, Daniel Chen, Zbigniew Kalbarczyk, Ravishankar K. Iyer
2011 conf
DSN Workshops
Jiesheng Wei, Layali Rashid, Karthik Pattabiraman, Sathish Gopalakrishnan
2011 A* conf
ASPLOS
Song Liu, Karthik Pattabiraman, Thomas Moscibroda, Benjamin G. Zorn
2011 A conf
ISSRE
Frolin S. Ocariza Jr., Karthik Pattabiraman, Benjamin G. Zorn
2011 A conf
CSF
Cole Schlesinger, Karthik Pattabiraman, Nikhil Swamy, David Walker, Benjamin G. Zorn
2010 A conf
ISSRE
Karthik Pattabiraman, Benjamin G. Zorn
2010 C conf
PRDC
Layali Rashid, Karthik Pattabiraman, Sathish Gopalakrishnan
2010 conf
DSN Workshops
Layali Rashid, Karthik Pattabiraman, Sathish Gopalakrishnan
2009 A conf
DSN
Galen Lyle, Shelley Cheny, Karthik Pattabiraman, Zbigniew Kalbarczyk, Ravishankar K. Iyer
2009
Karthik Pattabiraman
2009 B conf
PPoPP
Paruj Ratanaworabhan, Martin Burtscher, Darko Kirovski, Benjamin G. Zorn, Rahul Nagpal, Karthik Pattabiraman
2009 Misc conf
SEC
Karthik Pattabiraman, Nithin Nakka, Zbigniew Kalbarczyk, Ravishankar K. Iyer
2009 A conf
DSN
Karthik Pattabiraman, Zbigniew Kalbarczyk
2008 A conf
EuroSys
Karthik Pattabiraman, Vinod Grover, Benjamin G. Zorn
2008 A conf
DSN
Karthik Pattabiraman, Nithin Nakka, Zbigniew Kalbarczyk, Ravishankar K. Iyer
2008 A conf
DSN
Karthik Pattabiraman, Shuo Chen, Zbigniew Kalbarczyk
2007 C conf
IOLTS
Karthik Pattabiraman, Zbigniew Kalbarczyk, Ravishankar K. Iyer
2007 A conf
DSN
Nithin Nakka, Karthik Pattabiraman, Ravishankar K. Iyer
2007 J jnl
IEEE Secur. Priv.
Ravishankar K. Iyer, Zbigniew Kalbarczyk, Karthik Pattabiraman, William Healey, Wen-mei W. Hwu, Peter Klemperer, Reza Farivar
2006 Misc conf
EDCC
Karthik Pattabiraman, Giacinto Paolo Saggese, Daniel Chen, Zbigniew Kalbarczyk, Ravishankar K. Iyer
2005 C conf
PRDC
Karthik Pattabiraman, Zbigniew Kalbarczyk, Ravishankar K. Iyer
2005 A conf
DSN
Long Wang, Karthik Pattabiraman, Zbigniew Kalbarczyk, Ravishankar K. Iyer, Lawrence G. Votta, Christopher A. Vick, Alan Wood
2004 Misc conf
SEC
Shuo Chen, Karthik Pattabiraman, Zbigniew Kalbarczyk, Ravishankar K. Iyer
tests/unit/test_apk_extractors.py
← Index tests/unit/test_apk_extractors.py python
"""
Unit tests for APK extractors.

Tests tag(), get_clickhouse_table(), extract(), and prepare_export_data()
for all 8 APK extractors. Uses mocked androguard APK objects.
"""
import pytest
import os
import hashlib
import tempfile
import zipfile
from dataclasses import asdict
from unittest.mock import MagicMock, patch, PropertyMock

pytestmark = [pytest.mark.unit, pytest.mark.apk]


# ============================================================================
# Helpers
# ============================================================================

def _create_fake_apk(tmp_path, files=None):
    """Create a minimal valid ZIP file that pretends to be an APK.

    Args:
        tmp_path: Directory to write the fake APK into.
        files: Dict of {filename: content_bytes} to include in the ZIP.

    Returns:
        Path to the fake APK file.
    """
    apk_path = os.path.join(tmp_path, "test.apk")
    default_files = {
        "AndroidManifest.xml": b"<manifest/>",
        "classes.dex": b"dex\n035\x00" + b"\x00" * 100,
        "res/drawable/icon.png": b"\x89PNG" + b"\x00" * 50,
    }
    if files:
        default_files.update(files)

    with zipfile.ZipFile(apk_path, "w") as zf:
        for name, data in default_files.items():
            zf.writestr(name, data)
    return apk_path


def _make_mock_apk():
    """Create a mock androguard APK object with common return values."""
    mock_apk = MagicMock()
    mock_apk.get_package.return_value = "com.example.test"
    mock_apk.get_app_name.return_value = "Test App"
    mock_apk.get_androidversion_code.return_value = "10"
    mock_apk.get_androidversion_name.return_value = "1.2.3"
    mock_apk.get_min_sdk_version.return_value = "21"
    mock_apk.get_target_sdk_version.return_value = "33"
    mock_apk.get_effective_target_sdk_version.return_value = "33"
    mock_apk.get_main_activity.return_value = "com.example.test.MainActivity"
    mock_apk.get_attribute_value.return_value = None
    mock_apk.get_libraries.return_value = []
    mock_apk.get_files.return_value = [
        "AndroidManifest.xml", "classes.dex",
        "res/drawable/icon.png", "lib/armeabi-v7a/libnative.so",
        "assets/data.bin",
    ]
    mock_apk.get_permissions.return_value = [
        "android.permission.INTERNET",
        "android.permission.CAMERA",
    ]
    mock_apk.get_declared_permissions.return_value = {}
    mock_apk.get_activities.return_value = ["com.example.test.MainActivity"]
    mock_apk.get_services.return_value = ["com.example.test.MyService"]
    mock_apk.get_receivers.return_value = ["com.example.test.BootReceiver"]
    mock_apk.get_providers.return_value = []
    mock_apk.get_uses_implied_permission_list.return_value = []
    mock_apk.is_valid_APK.return_value = True
    mock_apk.get_certificates.return_value = []
    mock_apk.get_certificates_v1.return_value = []
    mock_apk.get_certificates_v2.return_value = []
    mock_apk.get_certificates_v3.return_value = []
    mock_apk.get_signature_names.return_value = []
    mock_apk.is_signed_v1.return_value = True
    mock_apk.is_signed_v2.return_value = True
    mock_apk.is_signed_v3.return_value = False
    return mock_apk


@pytest.fixture
def fake_apk_env(tmp_path):
    """Create a fake APK file and mock APK object."""
    apk_path = _create_fake_apk(str(tmp_path))
    mock_apk = _make_mock_apk()
    mock_logger = MagicMock()
    mock_exporters = [MagicMock()]
    return apk_path, mock_apk, mock_logger, mock_exporters


# ============================================================================
# APKFeaturesExtractor
# ============================================================================

class TestAPKFeaturesExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_features"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_features"

    def test_extract_returns_dataclass(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor
        from redb.models.dataclasses import APKFeatures

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKFeatures)
        assert result.package_name == "com.example.test"
        assert result.app_name == "Test App"
        assert result.version_code == 10
        assert result.version_name == "1.2.3"
        assert result.min_sdk_version == 21
        assert result.target_sdk_version == 33
        assert result.main_activity == "com.example.test.MainActivity"

    def test_extract_detects_native_code(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result.has_native_code is True
        assert result.has_assets is True

    def test_extract_invalid_apk_returns_none(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.is_valid_APK.return_value = False
        # APKExtractor sets self.apk to the mock_apk directly; _is_valid_apk checks self.apk is not None
        # For invalid APK, set apk to None
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=None)
        # Override _is_valid_apk to simulate parsing failure
        ext.apk = None
        result = ext.extract()
        assert result is None

    def test_extract_handles_missing_version_code(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.get_androidversion_code.return_value = None
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result.version_code == 0

    def test_prepare_export_data_clickhouse(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        ext.extract()
        result = ext.prepare_export_data("ClickHouseExporter")
        assert result is not None
        data, column_names, column_type_names = result
        assert "sha256" in column_names
        assert "package_name" in column_names
        assert "analysis_date" in column_names
        assert len(column_names) == len(column_type_names)
        assert len(data) == 1  # One row
        assert len(data[0]) == len(column_names)

    def test_prepare_export_data_no_extract(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        # Don't call extract() first
        result = ext.prepare_export_data("ClickHouseExporter")
        assert result is None


# ============================================================================
# APKManifestExtractor
# ============================================================================

class TestAPKManifestExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_manifest import APKManifestExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKManifestExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_manifest"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_manifest import APKManifestExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKManifestExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_manifest"

    def test_extract_returns_dataclass(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_manifest import APKManifestExtractor
        from redb.models.dataclasses import APKManifest

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # Setup intent filter mocking
        mock_apk.get_intent_filters.return_value = {}
        ext = APKManifestExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKManifest)
        assert result.activity_count == 1
        assert result.service_count == 1
        assert result.receiver_count == 1
        assert result.provider_count == 0

    def test_extract_counts_components(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_manifest import APKManifestExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.get_activities.return_value = ["A1", "A2", "A3"]
        mock_apk.get_services.return_value = ["S1", "S2"]
        mock_apk.get_intent_filters.return_value = {}
        ext = APKManifestExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result.activity_count == 3
        assert result.service_count == 2


# ============================================================================
# APKPermissionsExtractor
# ============================================================================

class TestAPKPermissionsExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_permissions"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_permissions"

    def test_extract_returns_dataclass(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor
        from redb.models.dataclasses import APKPermissions

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKPermissions)
        assert result.total_permission_count == 2
        assert "android.permission.INTERNET" in result.permissions
        assert "android.permission.CAMERA" in result.permissions

    def test_extract_classifies_dangerous(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        # CAMERA is dangerous, INTERNET is normal
        assert result.dangerous_permission_count >= 1
        assert "android.permission.CAMERA" in result.dangerous_permissions

    def test_extract_no_permissions(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_permissions import APKPermissionsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.get_permissions.return_value = []
        ext = APKPermissionsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result.total_permission_count == 0
        assert result.dangerous_permission_count == 0


# ============================================================================
# APKSignatureExtractor
# ============================================================================

class TestAPKSignatureExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_signature"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_signature"

    def test_extract_unsigned_apk(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor
        from redb.models.dataclasses import APKCodeSigningInfo

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.is_signed_v1.return_value = False
        mock_apk.is_signed_v2.return_value = False
        mock_apk.is_signed_v3.return_value = False
        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKCodeSigningInfo)

    def test_extract_detects_signature_schemes(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.is_signed_v1.return_value = True
        mock_apk.is_signed_v2.return_value = True
        mock_apk.is_signed_v3.return_value = True
        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert 1 in result.signature_scheme_versions
        assert 2 in result.signature_scheme_versions
        assert 3 in result.signature_scheme_versions

    def test_certificate_subject_uses_short_names(self, fake_apk_env):
        """Ensure subject/issuer use RFC 4514 short names (CN=, O=) not long names."""
        from unittest.mock import MagicMock
        from asn1crypto import x509
        from redb.extractors.apk_extractors.apk_signature import APKSignatureExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env

        # Build a real asn1crypto Name so the conversion is exercised
        subject_name = x509.Name.build({
            'country_name': 'US',
            'state_or_province_name': 'California',
            'organization_name': 'Android',
            'common_name': 'Android Debug',
        })

        mock_cert = MagicMock()
        mock_cert.subject = subject_name
        mock_cert.issuer = subject_name
        mock_cert.serial_number = 12345
        mock_cert.not_valid_before = '2020-01-01 00:00:00'
        mock_cert.not_valid_after = '2030-01-01 00:00:00'
        mock_cert.hash_algo = 'sha256'
        mock_cert.public_key.bit_size = 2048
        mock_cert.dump.return_value = b'fake_cert_der_data'

        mock_apk.is_signed_v1.return_value = False
        mock_apk.is_signed_v2.return_value = True
        mock_apk.is_signed_v3.return_value = False
        mock_apk.get_certificates_v2.return_value = [mock_cert]

        ext = APKSignatureExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert result.apk_signer_subject is not None
        assert 'CN=' in result.apk_signer_subject
        assert 'O=' in result.apk_signer_subject
        assert 'Common Name' not in result.apk_signer_subject
        assert 'Organization' not in result.apk_signer_subject

        # Also check the certificate list
        assert len(result.x509_certificates) == 1
        cert = result.x509_certificates[0]
        assert 'CN=' in cert.subject
        assert 'Common Name' not in cert.subject


# ============================================================================
# APKDexExtractor
# ============================================================================

class TestAPKDexExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_dex"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_dex"

    def test_extract_returns_list(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # Mock DEX data access
        mock_apk.get_dex_names.return_value = ["classes.dex"]
        mock_apk.get_all_dex.return_value = [b"dex\n035\x00" + b"\x00" * 100]

        # Mock the androguard DEX parser
        mock_dex_class = MagicMock()
        mock_dex_instance = MagicMock()
        mock_dex_instance.get_classes.return_value = []
        mock_dex_instance.get_methods.return_value = []
        mock_dex_instance.get_strings.return_value = []
        mock_dex_class.return_value = mock_dex_instance

        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        with patch.dict('sys.modules', {'androguard.core.dex': MagicMock(DEX=mock_dex_class)}):
            result = ext.extract()

        assert result is not None
        assert isinstance(result, list)
        assert len(result) == 1
        assert result[0].filename == "classes.dex"

    def test_extract_computes_tlsh(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # Need enough data for TLSH (>50 bytes of varied content)
        import os
        dex_data = os.urandom(256)
        mock_apk.get_dex_names.return_value = ["classes.dex"]
        mock_apk.get_all_dex.return_value = [dex_data]

        mock_dex_class = MagicMock()
        mock_dex_instance = MagicMock()
        mock_dex_instance.get_classes.return_value = []
        mock_dex_instance.get_methods.return_value = []
        mock_dex_instance.get_strings.return_value = []
        mock_dex_instance.get_encoded_methods.return_value = []
        mock_dex_class.return_value = mock_dex_instance

        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        with patch.dict('sys.modules', {'androguard.core.dex': MagicMock(DEX=mock_dex_class)}):
            result = ext.extract()

        assert result is not None
        assert result[0].tlsh is not None
        assert result[0].tlsh.startswith("T1")

    def test_extract_tlsh_none_for_small_dex(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # Too small / uniform for TLSH
        dex_data = b"\x00" * 10
        mock_apk.get_dex_names.return_value = ["classes.dex"]
        mock_apk.get_all_dex.return_value = [dex_data]

        mock_dex_class = MagicMock()
        mock_dex_instance = MagicMock()
        mock_dex_instance.get_classes.return_value = []
        mock_dex_instance.get_methods.return_value = []
        mock_dex_instance.get_strings.return_value = []
        mock_dex_instance.get_encoded_methods.return_value = []
        mock_dex_class.return_value = mock_dex_instance

        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        with patch.dict('sys.modules', {'androguard.core.dex': MagicMock(DEX=mock_dex_class)}):
            result = ext.extract()

        assert result is not None
        # TLSH returns empty string for insufficient data, which we convert to None
        assert result[0].tlsh is None

    def test_prepare_export_clickhouse_includes_tlsh(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor
        from redb.models.dataclasses import APKDexFile

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        # Manually set dex_files to test export
        ext.dex_files = [APKDexFile(
            filename="classes.dex",
            sha256="aa" * 32,
            class_count=10,
            method_count=50,
            string_count=100,
            tlsh="T1" + "B" * 68,
        )]

        result = ext.prepare_export_data("ClickHouseExporter")
        assert result is not None
        dex_export = result['dex']
        assert 'dex_tlsh' in dex_export['column_names']
        tlsh_idx = dex_export['column_names'].index('dex_tlsh')
        assert dex_export['data'][0][tlsh_idx] == "T1" + "B" * 68
        assert len(dex_export['column_names']) == len(dex_export['column_type_names'])

    def test_extract_no_dex_returns_none(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_dex import APKDexExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        mock_apk.get_dex_names.return_value = []
        mock_apk.get_all_dex.return_value = []
        ext = APKDexExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result is None


# ============================================================================
# APKResourceExtractor
# ============================================================================

class TestAPKResourceExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_resources"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_resources"

    def test_extract_returns_dict(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        # Returns dict with 'resources' key containing list of APKResource
        assert result is None or isinstance(result, dict)
        if result:
            assert "resources" in result
            assert "total_resource_count" in result
            assert isinstance(result["resources"], list)


# ============================================================================
# APKResourceExtractor — _identify_android_format
# ============================================================================

class TestIdentifyAndroidFormat:
    """Unit tests for Android-specific binary format magic detection."""

    def _make_extractor(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor
        apk_path, mock_apk, logger, exporters = fake_apk_env
        return APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

    def test_axml_magic(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'\x03\x00\x08\x00' + b'\x00' * 12
        assert ext._identify_android_format(header) == "android_binary_xml"

    def test_arsc_magic(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'\x02\x00\x0c\x00' + b'\x00' * 12
        assert ext._identify_android_format(header) == "android_resource_table"

    def test_art_prof_magic(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'pro\x00' + b'\x00' * 12
        assert ext._identify_android_format(header) == "android_art_profile"

    def test_art_profm_magic(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'prm\x00' + b'\x00' * 12
        assert ext._identify_android_format(header) == "android_art_profile_metadata"

    def test_unknown_magic_returns_none(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'\xff\xff\xff\xff' + b'\x00' * 12
        assert ext._identify_android_format(header) is None

    def test_short_header_returns_none(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._identify_android_format(b'\x03\x00') is None
        assert ext._identify_android_format(b'') is None

    def test_png_magic_not_matched(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        header = b'\x89PNG\r\n\x1a\n' + b'\x00' * 8
        assert ext._identify_android_format(header) is None


# ============================================================================
# APKResourceExtractor — _is_suspicious_resource
# ============================================================================

class TestIsSuspiciousResource:
    """Unit tests for the layered suspicious resource classification."""

    def _make_extractor(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor
        apk_path, mock_apk, logger, exporters = fake_apk_env
        return APKResourceExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

    # ── Layer 1: Allowlisted paths ──────────────────────────────────────

    def test_allowlisted_baseline_prof_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/dexopt/baseline.prof",
            magika_type="unknown", entropy=7.9,
            android_type=None,
        ) is False

    def test_allowlisted_baseline_profm_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/dexopt/baseline.profm",
            magika_type="unknown", entropy=7.9,
            android_type=None,
        ) is False

    # ── Layer 2: Android-specific formats → always benign ───────────────

    def test_axml_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/layout/activity_main.xml",
            magika_type="gzip", entropy=6.5,
            android_type="android_binary_xml",
        ) is False

    def test_arsc_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/values/resources.arsc",
            magika_type="unknown", entropy=7.2,
            android_type="android_resource_table",
        ) is False

    def test_art_profile_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/custom/baseline.prof",
            magika_type="unknown", entropy=7.8,
            android_type="android_art_profile",
        ) is False

    def test_art_profile_metadata_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/other/baseline.profm",
            magika_type="unknown", entropy=7.5,
            android_type="android_art_profile_metadata",
        ) is False

    # ── Layer 3: Image extension / Magika type mismatch ─────────────────

    def test_fake_png_extension_with_non_image_content_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/raw/payload.png",
            magika_type="zip", entropy=7.2,
            android_type=None,
        ) is True

    def test_fake_jpg_extension_with_unknown_high_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/data.jpg",
            magika_type="unknown", entropy=6.5,
            android_type=None,
        ) is True

    def test_image_extension_mismatch_low_entropy_not_suspicious(self, fake_apk_env):
        """Tiny/low-entropy files with image extensions shouldn't be flagged."""
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/drawable/tiny.png",
            magika_type="unknown", entropy=3.0,
            android_type=None,
        ) is False

    # ── Layer 4: Magika-confirmed images → benign ───────────────────────

    def test_confirmed_png_high_entropy_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/drawable/photo.png",
            magika_type="png", entropy=7.95,
            android_type=None,
        ) is False

    def test_confirmed_webp_high_entropy_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/drawable/image.webp",
            magika_type="webp", entropy=7.99,
            android_type=None,
        ) is False

    def test_confirmed_jpeg_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/photos/bg.jpg",
            magika_type="jpeg", entropy=7.9,
            android_type=None,
        ) is False

    # ── Layer 5: Known suspicious file types ────────────────────────────

    def test_elf_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/payload",
            magika_type="elf", entropy=5.0,
            android_type=None,
        ) is True

    def test_dex_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/classes.dex",
            magika_type="dex", entropy=5.5,
            android_type=None,
        ) is True

    def test_shell_script_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/run.sh",
            magika_type="shell", entropy=4.0,
            android_type=None,
        ) is True

    def test_zip_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/data.zip",
            magika_type="zip", entropy=7.5,
            android_type=None,
        ) is True

    def test_jar_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/plugin.jar",
            magika_type="jar", entropy=6.0,
            android_type=None,
        ) is True

    def test_lzma_in_assets_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/blob.lzma",
            magika_type="lzma", entropy=7.9,
            android_type=None,
        ) is True

    # ── Layer 6: High-entropy unrecognized blobs ────────────────────────

    def test_unknown_type_high_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/xyzdata",
            magika_type="unknown", entropy=7.5,
            android_type=None,
        ) is True

    def test_empty_type_high_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/blob.dat",
            magika_type="empty", entropy=7.2,
            android_type=None,
        ) is True

    def test_unknown_type_low_entropy_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/config.dat",
            magika_type="unknown", entropy=4.5,
            android_type=None,
        ) is False

    # ── Layer 7: Extreme entropy on non-image recognized types ──────────

    def test_xml_extreme_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/raw/config.xml",
            magika_type="xml", entropy=7.9,
            android_type=None,
        ) is True

    def test_txt_extreme_entropy_is_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/readme.txt",
            magika_type="txt", entropy=7.86,
            android_type=None,
        ) is True

    def test_xml_normal_entropy_not_suspicious(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="res/raw/config.xml",
            magika_type="xml", entropy=4.0,
            android_type=None,
        ) is False

    # ── Benign types that should NOT be flagged ─────────────────────────

    def test_html_not_in_suspicious_types(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/index.html",
            magika_type="html", entropy=5.0,
            android_type=None,
        ) is False

    def test_javascript_not_in_suspicious_types(self, fake_apk_env):
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/app.js",
            magika_type="javascript", entropy=5.0,
            android_type=None,
        ) is False

    def test_sqlite_not_in_suspicious_types(self, fake_apk_env):
        """sqlite was intentionally excluded — too many legitimate uses."""
        ext = self._make_extractor(fake_apk_env)
        assert ext._is_suspicious_resource(
            path="assets/data.db",
            magika_type="sqlite", entropy=5.0,
            android_type=None,
        ) is False


# ============================================================================
# APKResourceExtractor — integration with extract()
# ============================================================================

class TestAPKResourceExtractorIntegration:
    """Integration tests: craft ZIP contents and verify end-to-end classification."""

    def test_extract_flags_elf_in_assets(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path = _create_fake_apk(str(tmp_path), {
            "assets/payload.bin": b"\x7fELF" + b"\x00" * 100,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        # Mock Magika to return "elf" for the payload — real Magika needs
        # a valid ELF structure, but we control classification in unit tests.
        mock_magika = MagicMock()
        mock_magika.identify_bytes.return_value = MagicMock(
            output=MagicMock(label="elf")
        )

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        with patch("magika.Magika", return_value=mock_magika):
            result = ext.extract()
        assert result is not None
        assert result["suspicious_file_count"] >= 1
        sus_paths = [s.path for s in result["suspicious_files"]]
        assert "assets/payload.bin" in sus_paths

    def test_extract_does_not_flag_normal_png(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path = _create_fake_apk(str(tmp_path), {
            "res/drawable/icon.png": b"\x89PNG\r\n\x1a\n" + b"\x00" * 50,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        result = ext.extract()
        assert result is not None
        sus_paths = [s.path for s in result["suspicious_files"]]
        assert "res/drawable/icon.png" not in sus_paths

    def test_extract_stores_android_type_override(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        # AXML magic — Magika would likely misclassify, but stored_type
        # should be "android_binary_xml"
        axml_data = b'\x03\x00\x08\x00' + b'\x00' * 50
        apk_path = _create_fake_apk(str(tmp_path), {
            "res/layout/main.xml": axml_data,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        result = ext.extract()
        assert result is not None
        axml_resources = [r for r in result["resources"]
                          if r.path == "res/layout/main.xml"]
        assert len(axml_resources) == 1
        assert axml_resources[0].filetype_magika == "android_binary_xml"
        assert axml_resources[0].is_suspicious is False

    def test_extract_skips_non_res_non_assets(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path = _create_fake_apk(str(tmp_path), {
            "lib/armeabi-v7a/libnative.so": b"\x7fELF" + b"\x00" * 50,
            "META-INF/MANIFEST.MF": b"Manifest-Version: 1.0\n",
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        result = ext.extract()
        if result:
            res_paths = [r.path for r in result["resources"]]
            assert "lib/armeabi-v7a/libnative.so" not in res_paths
            assert "META-INF/MANIFEST.MF" not in res_paths

    def test_prepare_export_includes_suspicious_flag(self, tmp_path):
        from redb.extractors.apk_extractors.apk_resources import APKResourceExtractor

        apk_path = _create_fake_apk(str(tmp_path), {
            "assets/evil.bin": b"\x7fELF" + b"\x00" * 100,
            "res/drawable/ok.png": b"\x89PNG\r\n\x1a\n" + b"\x00" * 50,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()

        # Map filenames to Magika labels so the ELF payload gets flagged
        def _magika_side_effect(data):
            if data[:4] == b"\x7fELF":
                return MagicMock(output=MagicMock(label="elf"))
            return MagicMock(output=MagicMock(label="png"))

        mock_magika = MagicMock()
        mock_magika.identify_bytes.side_effect = _magika_side_effect

        ext = APKResourceExtractor(apk_path, logger, exporters=[MagicMock()], apk=mock_apk)
        with patch("magika.Magika", return_value=mock_magika):
            ext.extract()
        result = ext.prepare_export_data("ClickHouseExporter")
        assert result is not None
        data, col_names, col_types = result
        assert "is_suspicious" in col_names
        sus_idx = col_names.index("is_suspicious")
        # At least one row should have is_suspicious=1
        sus_values = [row[sus_idx] for row in data]
        assert 1 in sus_values


# ============================================================================
# APKNativeLibExtractor
# ============================================================================

class TestAPKNativeLibExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_native_libs"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_native_libs"

    def test_extract_finds_native_libs(self, tmp_path):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor
        from redb.models.dataclasses import APKNativeLib

        # Create APK with a native lib inside
        apk_path = _create_fake_apk(str(tmp_path), {
            "lib/armeabi-v7a/libnative.so": b"\x7fELF" + b"\x00" * 100,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()
        exporters = [MagicMock()]

        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result is not None
        assert isinstance(result, dict)
        assert "native_libs" in result
        assert len(result["native_libs"]) >= 1
        assert isinstance(result["native_libs"][0], APKNativeLib)
        assert result["native_libs"][0].abi == "armeabi-v7a"

    def test_extract_detects_packer_libs(self, tmp_path):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor

        # Create APK with a known packer lib
        apk_path = _create_fake_apk(str(tmp_path), {
            "lib/armeabi-v7a/libjiagu.so": b"\x7fELF" + b"\x00" * 50,
            "lib/armeabi-v7a/libnormal.so": b"\x7fELF" + b"\x00" * 50,
        })
        mock_apk = _make_mock_apk()
        logger = MagicMock()
        exporters = [MagicMock()]

        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result is not None
        packer_libs = [lib for lib in result["native_libs"] if lib.is_known_packer]
        assert len(packer_libs) >= 1  # libjiagu.so is a known packer

    def test_extract_no_native_libs(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_native_libs import APKNativeLibExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        # The fake APK has no lib/*.so files
        mock_apk.get_files.return_value = ["classes.dex", "AndroidManifest.xml"]
        ext = APKNativeLibExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        assert result is None


# ============================================================================
# APKInconsistencyTestsExtractor
# ============================================================================

class TestAPKInconsistencyTestsExtractor:

    def test_tag(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_inconsistency_tests import APKInconsistencyTestsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKInconsistencyTestsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.tag() == "apk_inconsistency_tests"

    def test_clickhouse_table(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_inconsistency_tests import APKInconsistencyTestsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKInconsistencyTestsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext.get_clickhouse_table() == "redb_apk_inconsistency_tests"

    def test_extract_returns_dataclass(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_inconsistency_tests import APKInconsistencyTestsExtractor
        from redb.models.dataclasses import APKInconsistencyTests

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKInconsistencyTestsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()

        assert isinstance(result, APKInconsistencyTests)

    def test_extract_clean_apk_no_anomalies(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_inconsistency_tests import APKInconsistencyTestsExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKInconsistencyTestsExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        result = ext.extract()
        # A clean APK should have mostly False values
        assert result.test_zip_bomb is not None  # Should be set (True or False)


# ============================================================================
# APK workers.py integration — module registry
# ============================================================================

class TestAPKModuleRegistry:

    def test_all_apk_modules_in_registry(self):
        from redb.workers import get_module_by_name

        apk_modules = [
            "APKFeaturesExtractor",
            "APKManifestExtractor",
            "APKPermissionsExtractor",
            "APKSignatureExtractor",
            "APKDexExtractor",
            "APKResourceExtractor",
            "APKNativeLibExtractor",
            "APKInconsistencyTestsExtractor",
        ]
        for name in apk_modules:
            module_class = get_module_by_name(name)
            assert module_class is not None, f"{name} not found in module registry"

    def test_apk_module_classes_correct(self):
        from redb.workers import get_module_by_name
        from redb.extractors.apk_extractors import (
            APKFeaturesExtractor,
            APKManifestExtractor,
            APKPermissionsExtractor,
            APKSignatureExtractor,
            APKDexExtractor,
            APKResourceExtractor,
            APKNativeLibExtractor,
            APKInconsistencyTestsExtractor,
        )

        assert get_module_by_name("APKFeaturesExtractor") is APKFeaturesExtractor
        assert get_module_by_name("APKManifestExtractor") is APKManifestExtractor
        assert get_module_by_name("APKPermissionsExtractor") is APKPermissionsExtractor
        assert get_module_by_name("APKSignatureExtractor") is APKSignatureExtractor
        assert get_module_by_name("APKDexExtractor") is APKDexExtractor
        assert get_module_by_name("APKResourceExtractor") is APKResourceExtractor
        assert get_module_by_name("APKNativeLibExtractor") is APKNativeLibExtractor
        assert get_module_by_name("APKInconsistencyTestsExtractor") is APKInconsistencyTestsExtractor


# ============================================================================
# APK filetype guard — workers.py dispatch
# ============================================================================

class TestAPKWorkerDispatch:

    def test_apk_excluded_from_die_guard(self):
        """APK filetype should be in the exclusion set alongside macho for DIE/BasicProperties/Hash."""
        import redb.workers as workers_mod
        import inspect

        source = inspect.getsource(workers_mod.process_binary_file)
        # The guards should exclude "apk" from DIE, BasicProperties, HashExtractor.
        # The exact tuple may grow as new text-based formats are added (e.g.
        # "javascript"), so we check that "apk" is present in a filetype
        # exclusion guard rather than matching the full tuple literally.
        assert 'filetype not in ("macho", "apk"' in source

    def test_apk_dispatch_branch_exists(self):
        """The APK dispatch branch should exist and registry should provide all APK extractors."""
        import inspect
        import redb.workers as workers_mod
        from redb.extractor_registry import get_filetype_modules

        # Verify the APK branch exists in process_binary_file
        source = inspect.getsource(workers_mod.process_binary_file)
        assert 'elif filetype == "apk"' in source

        # Verify all APK extractors are registered and loadable
        apk_modules = get_filetype_modules("apk")
        apk_names = {m.__name__ for m in apk_modules}
        for expected in [
            "APKFeaturesExtractor", "APKManifestExtractor",
            "APKPermissionsExtractor", "APKSignatureExtractor",
            "APKDexExtractor", "APKResourceExtractor",
            "APKNativeLibExtractor", "APKInconsistencyTestsExtractor",
        ]:
            assert expected in apk_names, f"{expected} not in APK registry"

    def test_apk_dispatch_runs_basic_and_hash(self):
        """The APK dispatch branch should run BasicPropertiesExtractor and HashExtractor."""
        import redb.workers as workers_mod
        import inspect

        source = inspect.getsource(workers_mod.process_binary_file)
        # Find the apk block
        apk_idx = source.index('elif filetype == "apk"')
        apk_block = source[apk_idx:apk_idx + 4000]
        assert "BasicPropertiesExtractor" in apk_block
        assert "HashExtractor" in apk_block


# ============================================================================
# APKExtractor base class
# ============================================================================

class TestAPKExtractorBase:

    def test_binary_property(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        # binary property should return the file content
        with open(apk_path, "rb") as f:
            expected = f.read()
        assert ext.binary == expected

    def test_is_valid_apk_with_apk(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        assert ext._is_valid_apk() is True

    def test_is_valid_apk_without_apk(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, _, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=None)
        ext.apk = None
        assert ext._is_valid_apk() is False

    def test_list_files(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        files = ext._list_files()
        assert len(files) == 5
        assert "classes.dex" in files

    def test_get_zip_file(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)
        zf = ext._get_zip_file()
        assert zf is not None
        zf.close()

    def test_sha256_computed_correctly(self, fake_apk_env):
        from redb.extractors.apk_extractors.apk_features import APKFeaturesExtractor

        apk_path, mock_apk, logger, exporters = fake_apk_env
        ext = APKFeaturesExtractor(apk_path, logger, exporters=exporters, apk=mock_apk)

        # Verify sha256 matches actual file content
        with open(apk_path, "rb") as f:
            expected_sha256 = hashlib.sha256(f.read()).hexdigest()
        assert ext.sha256 == expected_sha256