M. Mallar Chakravarty

51 papers Journal 44Unranked 7
YearRankTypeTitle / Venue / Authors
2024 J jnl
Medical Image Anal.
Simon Oxenford, Ana Sofía Ríos, Barbara Hollunder, Clemens Neudorfer, Alexandre Boutet, Gavin J. B. Elias, Jürgen Germann, Aaron Loh, Wissam Deeb, Bryan Salvato, Leonardo Almeida, Kelly D. Foote, Robert S. C. Amaral, Paul B. Rosenberg, David F. Tang-Wai, David A. Wolk, Anna D. Burke, Marwan N. Sabbagh, Stephen Salloway, M. Mallar Chakravarty, Gwenn S. Smith, Constantine G. Lyketsos, Michael S. Okun, William S. Anderson, Zoltan Mari, Francisco A. Ponce, Andres M. Lozano, Wolf-Julian Neumann, Bassam Al-Fatly, Andreas Horn
2023 J jnl
NeuroImage
Hadis Kalantar Hormozi, Raihaan Patel, Alyssa Dai, Justine Ziolkowski, Hao-Ming Dong, Avram J. Holmes, Armin Raznahan, Gabriel A. Devenyi, M. Mallar Chakravarty
2023 J jnl
NeuroImage
Vanessa Valiquette, Elisa Guma, Lani Cupo, Daniel Gallino, Chloe Anastassiadis, Emily Snook, Gabriel A. Devenyi, M. Mallar Chakravarty
2022 J jnl
NeuroImage
Corinne Robert, Raihaan Patel, Nadia Blostein, Chrisopher J. Steele, M. Mallar Chakravarty
2022 J jnl
NeuroImage
Raihaan Patel, Clare E. Mackay, Michelle G. Jansen, Gabriel A. Devenyi, M. Clare O'Donoghue, Mika Kivimäki, Archana Singh-Manoux, Eniko Zsoldos, Klaus P. Ebmeier, M. Mallar Chakravarty, Sana Suri
2022 J jnl
NeuroImage
Paul-Noel Rousseau, M. Mallar Chakravarty, Christopher John Steele
2021 J jnl
NeuroImage
Athina Tzovara, Ishmael Amarreh, Valentina Borghesani, M. Mallar Chakravarty, Elizabeth DuPre, Christian Grefkes, Amelie Haugg, Lee Jollans, Hyang Woon Lee, Sharlene D. Newman, Rosanna K. Olsen, J. Tilak Ratnanather, Gina Rippon, Lucina Q. Uddin, Maria L. Bringas-Vega, Michele Veldsman, Tonya White, AmanPreet Badhwar
2021 J jnl
NeuroImage
Aurélie Bussy, Eric Plitman, Raihaan Patel, Stephanie Tullo, Alyssa Salaciak, Saashi A. Bedford, Sarah Farzin, Marie-Lise Béland, Vanessa Valiquette, Christina Kazazian, Christine Lucas Tardif, Gabriel A. Devenyi, M. Mallar Chakravarty
2021 J jnl
NeuroImage
Stefan Drakulich, Anne-Charlotte Thiffault, Emily R. Olafson, Olivier Parent, Aurélie Labbe, Matthew D. Albaugh, Budhachandra S. Khundrakpam, Simon Ducharme, Alan C. Evans, M. Mallar Chakravarty, Sherif Karama
2021 J jnl
NeuroImage
Eric Plitman, Aurélie Bussy, Vanessa Valiquette, Alyssa Salaciak, Raihaan Patel, Lani Cupo, Marie-Lise Béland, Stephanie Tullo, Christine Lucas Tardif, M. Natasha Rajah, Jamie Near, Gabriel A. Devenyi, M. Mallar Chakravarty
2020 J jnl
NeuroImage
Raihaan Patel, Christopher John Steele, Anthony G. X. Chen, Sejal Patel, Gabriel A. Devenyi, Jürgen Germann, Christine L. Tardif, M. Mallar Chakravarty
2020 J jnl
NeuroImage
Ari M. Fish, Ajay Nadig, Jakob Seidlitz, Paul K. Reardon, Catherine Mankiw, Cassidy L. McDermott, Jonathan D. Blumenthal, Liv S. Clasen, François Lalonde, Jason P. Lerch, M. Mallar Chakravarty, Russell T. Shinohara, Armin Raznahan
2019 J jnl
Frontiers Neuroinformatics
Francesca Mandino, Domenic H. Cerri, Clément M. Garin, Milou Straathof, Geralda A. F. van Tilborg, M. Mallar Chakravarty, Marc Dhenain, Rick M. Dijkhuizen, Alessandro Gozzi, Andreas Hess, Shella D. Keilholz, Jason P. Lerch, Yen-Yu Ian Shih, Joanes Grandjean
2019 J jnl
NeuroImage
Ting Guo, Vann Chau, Shabnam Peyvandi, Beatrice Latal, Patrick S. McQuillen, Walter Knirsch, Anne Synnes, Maria Feldmann, Nadja Naef, M. Mallar Chakravarty, Alessandra De Petrillo, Emma G. Duerden, A. James Barkovich, Steven P. Miller
2018 J jnl
NeuroImage
Dorothee Schoemaker, Claudia Buss, Kevin Head, Curt A. Sandman, Elysia Poggi Davis, M. Mallar Chakravarty, Serge Gauthier, Jens C. Pruessner
2018 J jnl
NeuroImage
Carolina Makowski, Sophie Béland, Penelope Kostopoulos, Nikhil Bhagwat, Gabriel A. Devenyi, Ashok K. Malla, Ridha Joober, Martin D. Lepage, M. Mallar Chakravarty
2018 J jnl
NeuroImage
Christopher John Steele, M. Mallar Chakravarty
2018 J jnl
NeuroImage
Sejal Patel, Raihaan Patel, Min Tae M. Park, Mario Masellis, Jo Knight, M. Mallar Chakravarty
2018 J jnl
CoRR
Ahmed Ashraf, Shehroz S. Khan, Nikhil Bhagwat, M. Mallar Chakravarty, Babak Taati
2018 J jnl
NeuroImage
Robert S. C. Amaral, Min Tae M. Park, Gabriel A. Devenyi, Vivian Lynn, Jon Pipitone, Julie L. Winterburn, Sofia Chavez, Mark M. Schira, Nancy J. Lobaugh, Aristotle N. Voineskos, Jens C. Pruessner, M. Mallar Chakravarty
2018 J jnl
PLoS Comput. Biol.
Nikhil Bhagwat, Joseph D. Viviano, Aristotle N. Voineskos, M. Mallar Chakravarty, Alzheimer's Disease Neuroimaging Initiative
2018 J jnl
NeuroImage
Elisa Guma, Jill Rocchetti, Gabriel A. Devenyi, Arnaud Tanti, Axel Mathieu, Jason P. Lerch, Guillaume Elgbeili, Blandine Courcot, Naguib Mechawar, M. Mallar Chakravarty, Bruno Giros
2018 J jnl
NeuroImage
Siobhan Ewert, Philip Plettig, Ningfei Li, M. Mallar Chakravarty, D. Louis Collins, Todd M. Herrington, Andrea A. Kühn, Andreas Horn
2017 J jnl
PLoS Comput. Biol.
Krzysztof J. Gorgolewski, Fidel Alfaro-Almagro, Tibor Auer, Pierre Bellec, Mihai Capota, M. Mallar Chakravarty, Nathan William Churchill, Alexander Li Cohen, R. Cameron Craddock, Gabriel A. Devenyi, Anders Eklund, Oscar Esteban, Guillaume Flandin, Satrajit S. Ghosh, J. Swaroop Guntupalli, Mark Jenkinson, Anisha Keshavan, Gregory Kiar, Franziskus Liem, Pradeep Reddy Raamana, David Raffelt, Christopher John Steele, Pierre-Olivier Quirion, Robert E. Smith, Stephen C. Strother, Gaël Varoquaux, Yida Wang, Tal Yarkoni, Russell A. Poldrack
2017 J jnl
NeuroImage
José E. Romero, Pierrick Coupé, Rémi Giraud, Vinh-Thong Ta, Vladimir S. Fonov, Min Tae M. Park, M. Mallar Chakravarty, Aristotle N. Voineskos, José V. Manjón
2016 J jnl
NeuroImage
M. Mallar Chakravarty, Clement Hamani, Alonso Martinez-Canabal, Jacob Ellegood, Christine L. Laliberté, José N. Nobrega, Tejas Sankar, Andres M. Lozano, Paul W. Frankland, Jason P. Lerch
2016 J jnl
Frontiers Neuroinformatics
Sejal Patel, Min Tae M. Park, M. Mallar Chakravarty, Jo Knight
2016 J jnl
NeuroImage
Dorothee Schoemaker, Claudia Buss, Kevin Head, Curt A. Sandman, Elysia Poggi Davis, M. Mallar Chakravarty, Serge Gauthier, Jens C. Pruessner
2015 conf
ISBI
Nikhil Bhagwat, Jon Pipitone, Aristotle N. Voineskos, Jens C. Pruessner, M. Mallar Chakravarty
2015 J jnl
Int. J. Comput. Assist. Radiol. Surg.
Yiming Xiao, Vladimir S. Fonov, Silvain Bériault, Fahd Al Subaie, M. Mallar Chakravarty, Abbas F. Sadikot, G. Bruce Pike, D. Louis Collins
2015 J jnl
NeuroImage
Paul A. Yushkevich, Robert S. C. Amaral, Jean C. Augustinack, Andrew R. Bender, Jeffrey D. Bernstein, Marina Boccardi, Martina Bocchetta, Alison C. Burggren, Valerie A. Carr, M. Mallar Chakravarty, Gaël Chételat, Ana M. Daugherty, Lila Davachi, Song-Lin Ding, Arne D. Ekstrom, Mirjam I. Geerlings, Abdul Hassan, Yushan Huang, Juan Eugenio Iglesias, Renaud La Joie, Geoffrey A. Kerchner, Karen F. LaRocque, Laura A. Libby, Nikolai Malykhin, Susanne G. Mueller, Rosanna K. Olsen, Daniela J. Palombo, Mansi B. Parekh
2015 J jnl
NeuroImage
Lawrence H. Baer, Min Tae M. Park, Jennifer Anne Bailey, M. Mallar Chakravarty, K. Z. H. Li, Virginia B. Penhune
2014 J jnl
NeuroImage
Min Tae M. Park, Jon Pipitone, Lawrence H. Baer, Julie L. Winterburn, Yashvi Shah, Sofia Chavez, Mark M. Schira, Nancy J. Lobaugh, Jason P. Lerch, Aristotle N. Voineskos, M. Mallar Chakravarty
2014 J jnl
NeuroImage
Angelita Pui-Yee Wong, Jon Pipitone, Min Tae M. Park, Erin W. Dickie, Gabriel Leonard, Michel Perron, G. Bruce Pike, Louis Richer, Suzanne Veillette, M. Mallar Chakravarty, Zdenka Pausova, Tomás Paus
2014 J jnl
NeuroImage
Jon Pipitone, Min Tae M. Park, Julie L. Winterburn, Tristram A. Lett, Jason P. Lerch, Jens C. Pruessner, Martin D. Lepage, Aristotle N. Voineskos, M. Mallar Chakravarty
2014 J jnl
Frontiers Neuroinformatics
Miriam Friedel, Matthijs C. van Eede, Jon Pipitone, M. Mallar Chakravarty, Jason P. Lerch
2013 J jnl
NeuroImage
Julie L. Winterburn, Jens C. Pruessner, Sofia Chavez, Mark M. Schira, Nancy J. Lobaugh, Aristotle N. Voineskos, M. Mallar Chakravarty
2013 J jnl
NeuroImage
Klára Marecková, M. Mallar Chakravarty, Mei Huang, Claire Lawrence, Gabriel Leonard, Michel Perron, G. Bruce Pike, Louis Richer, Suzanne Veillette, Zdenka Pausova, Tomás Paus
2013 J jnl
NeuroImage
Matthijs C. van Eede, Jan Scholz, M. Mallar Chakravarty, R. Mark Henkelman, Jason P. Lerch
2012 conf
IPCAI
Yiming Xiao, Lara Bailey, M. Mallar Chakravarty, Silvain Bériault, Abbas F. Sadikot, G. Bruce Pike, D. Louis Collins
2011 J jnl
NeuroImage
Stephen Frey, Deepak N. Pandya, M. Mallar Chakravarty, Lara Bailey, Michael Petrides, D. Louis Collins
2010 J jnl
J. Cogn. Neurosci.
Sandra E. Leh, Alain Ptito, Marc Schönwiesner, M. Mallar Chakravarty, Kathy T. Mullen
2010 J jnl
NeuroImage
Sune Nørhøj Jespersen, Carsten Reidies Bjarkam, Jens R. Nyengaard, M. Mallar Chakravarty, Brian Hansen, Thomas Vosegaard, Leif Østergaard, Dmitriy A. Yablonskiy, Niels Chr. Nielsen, Peter Vestergaard-Poulsen
2008 conf
ISBI
M. Mallar Chakravarty, Pedro Rosa-Neto, Scott Broadbent, Alan C. Evans, D. Louis Collins
2008 J jnl
Int. J. Biomed. Imaging
Sandra E. Leh, M. Mallar Chakravarty, Alain Ptito
2008 conf
ISBI
M. Mallar Chakravarty, Barry J. Bedell, Simone P. Zehntner, Alan C. Evans, D. Louis Collins
2008 J jnl
Medical Image Anal.
M. Mallar Chakravarty, Abbas F. Sadikot, Jürgen Germann, Gilles Bertrand, D. Louis Collins
2006 J jnl
NeuroImage
M. Mallar Chakravarty, Gilles Bertrand, Charles P. Hodge, Abbas F. Sadikot, D. Louis Collins
2006 conf
MICCAI (2)
M. Mallar Chakravarty, Abbas F. Sadikot, Sanjay Mongia, Gilles Bertrand, D. Louis Collins
2005 conf
MICCAI (2)
M. Mallar Chakravarty, Abbas F. Sadikot, Jürgen Germann, Gilles Bertrand, D. Louis Collins
2003 conf
MICCAI (1)
M. Mallar Chakravarty, Gilles Bertrand, Maxime Descoteaux, Abbas F. Sadikot, D. Louis Collins
tests/unit/test_apk_strings_ioc.py
← Index tests/unit/test_apk_strings_ioc.py python
"""Tests for APK string extraction and IOC extraction integration.

Covers:
- APKCodeAnalyzer._extract_strings() — string extraction from DEX objects
- APKCodeAnalyzer._string_entropy() — Shannon entropy computation
- DecompileAPK strings export table schema
- IOCExtractorFromResults with APK-format analysis_results
- Worker IOC wiring for APK
"""
import os
import tempfile
from dataclasses import asdict
from unittest.mock import MagicMock, patch

import pytest

from redb.extractors.ioc_extractor.ioc_extractor import IOCExtractorFromResults
from redb.extractors.ioc_extractor.standalone_ioc_extractor import (
    IOCType,
    SourceType,
)

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


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_analyzer(min_instructions=1):
    """Create an APKCodeAnalyzer with mocked dependencies."""
    with patch(
        "redb.extractors.decompiler.apk.analyzer.JADXDecompiler"
    ), patch(
        "redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler"
    ):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        analyzer = APKCodeAnalyzer("/fake/path.apk")
        analyzer.min_instructions = min_instructions
        return analyzer


def _make_mock_dex(strings):
    """Create a mock DEX object that returns the given strings."""
    dex = MagicMock()
    dex.get_strings.return_value = strings
    return dex


def _create_fake_apk_file():
    """Create a temporary file to act as a fake APK."""
    fd, path = tempfile.mkstemp(suffix=".apk")
    os.write(fd, b"PK\x03\x04fake apk content for hashing")
    os.close(fd)
    return path


# ---------------------------------------------------------------------------
# Tests: _string_entropy
# ---------------------------------------------------------------------------

class TestStringEntropy:
    """Tests for Shannon entropy computation."""

    def test_empty_string(self):
        analyzer = _make_analyzer()
        assert analyzer._string_entropy("") == 0.0

    def test_single_char(self):
        analyzer = _make_analyzer()
        assert analyzer._string_entropy("a") == 0.0

    def test_repeated_char(self):
        analyzer = _make_analyzer()
        assert analyzer._string_entropy("aaaa") == 0.0

    def test_two_equal_chars(self):
        analyzer = _make_analyzer()
        # "ab" -> entropy = 1.0
        assert abs(analyzer._string_entropy("ab") - 1.0) < 0.01

    def test_higher_entropy(self):
        analyzer = _make_analyzer()
        # More variety = higher entropy
        low = analyzer._string_entropy("aabb")
        high = analyzer._string_entropy("abcd")
        assert high > low

    def test_url_string_entropy(self):
        analyzer = _make_analyzer()
        entropy = analyzer._string_entropy("https://evil.com/payload")
        assert entropy > 2.0  # URLs have moderate entropy


# ---------------------------------------------------------------------------
# Tests: _extract_strings
# ---------------------------------------------------------------------------

class TestExtractStrings:
    """Tests for DEX string extraction."""

    def test_basic_extraction(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["hello", "world"])
        result = analyzer._extract_strings([dex])

        assert len(result) == 2
        assert result[0]["string"] == "hello"
        assert result[1]["string"] == "world"

    def test_string_fields_present(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["test_string"])
        result = analyzer._extract_strings([dex])

        entry = result[0]
        assert "string" in entry
        assert "string_encoding" in entry
        assert "string_offset" in entry
        assert "string_length" in entry
        assert "string_entropy" in entry
        assert entry["string_encoding"] == "UTF8"
        assert entry["string_length"] == len("test_string")

    def test_deduplication_across_dex(self):
        """Same string in multiple DEX files is only extracted once."""
        analyzer = _make_analyzer()
        dex1 = _make_mock_dex(["shared", "unique1"])
        dex2 = _make_mock_dex(["shared", "unique2"])
        result = analyzer._extract_strings([dex1, dex2])

        string_values = [s["string"] for s in result]
        assert string_values.count("shared") == 1
        assert "unique1" in string_values
        assert "unique2" in string_values
        assert len(result) == 3

    def test_empty_strings_filtered(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["", "valid", None, "also_valid"])
        result = analyzer._extract_strings([dex])

        string_values = [s["string"] for s in result]
        assert "" not in string_values
        assert None not in string_values
        assert "valid" in string_values
        assert "also_valid" in string_values

    def test_no_dex_files(self):
        analyzer = _make_analyzer()
        result = analyzer._extract_strings([])
        assert result == []

    def test_dex_get_strings_returns_none(self):
        analyzer = _make_analyzer()
        dex = MagicMock()
        dex.get_strings.return_value = None
        result = analyzer._extract_strings([dex])
        assert result == []

    def test_dex_get_strings_exception(self):
        analyzer = _make_analyzer()
        dex = MagicMock()
        dex.get_strings.side_effect = RuntimeError("corrupt DEX")
        result = analyzer._extract_strings([dex])
        assert result == []

    def test_entropy_computed(self):
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["abcdefgh"])
        result = analyzer._extract_strings([dex])
        assert result[0]["string_entropy"] > 0.0

    def test_incremental_offsets(self):
        """String offsets are incremented sequentially."""
        analyzer = _make_analyzer()
        dex = _make_mock_dex(["a", "b", "c"])
        result = analyzer._extract_strings([dex])

        offsets = [s["string_offset"] for s in result]
        assert offsets == [0, 1, 2]

    def test_url_strings_preserved(self):
        """URL strings are extracted without modification."""
        analyzer = _make_analyzer()
        dex = _make_mock_dex([
            "https://evil.com/payload",
            "http://c2.malware.org/gate",
        ])
        result = analyzer._extract_strings([dex])
        strings = [s["string"] for s in result]
        assert "https://evil.com/payload" in strings
        assert "http://c2.malware.org/gate" in strings


# ---------------------------------------------------------------------------
# Tests: strings in extract() integration
# ---------------------------------------------------------------------------

class TestExtractStringsIntegration:
    """Tests that strings are included in extract() results."""

    @patch("redb.extractors.decompiler.apk.analyzer.APKCodeAnalyzer._run_androguard")
    @patch("redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler")
    @patch("redb.extractors.decompiler.apk.analyzer.JADXDecompiler")
    def test_extract_includes_strings(self, mock_jadx, mock_apktool, mock_androguard):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer

        mock_dex = _make_mock_dex(["hello", "world"])
        mock_analysis = MagicMock()
        mock_analysis.get_methods.return_value = []
        mock_androguard.return_value = (MagicMock(), [mock_dex], mock_analysis)

        analyzer = APKCodeAnalyzer("/fake/path.apk", log=MagicMock())
        results = analyzer.extract()

        assert "strings" in results
        assert len(results["strings"]) == 2
        assert results["strings"][0]["string"] == "hello"
        analyzer.cleanup()

    @patch("redb.extractors.decompiler.apk.analyzer.APKCodeAnalyzer._run_androguard")
    @patch("redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler")
    @patch("redb.extractors.decompiler.apk.analyzer.JADXDecompiler")
    def test_extract_strings_error_is_non_fatal(self, mock_jadx, mock_apktool, mock_androguard):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer

        # Return a bad dex list that will cause _extract_strings to fail
        mock_analysis = MagicMock()
        mock_analysis.get_methods.return_value = []
        # dexs is not iterable — triggers the except block
        mock_androguard.return_value = (MagicMock(), 42, mock_analysis)

        analyzer = APKCodeAnalyzer("/fake/path.apk", log=MagicMock())
        results = analyzer.extract()

        # Should not crash, strings should be empty, error recorded
        assert results["strings"] == []
        assert any(
            e.get("error_location") == "strings"
            for e in results["analysis_errors"]
        )
        analyzer.cleanup()


# ---------------------------------------------------------------------------
# Tests: DecompileAPK strings export table
# ---------------------------------------------------------------------------

class TestDecompileAPKStringsExport:
    """Tests for strings table in DecompileAPK.prepare_export_data()."""

    def _make_extractor_with_strings(self, strings_data):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        ext = DecompileAPK(apk_path, MagicMock())
        ext.analysis_results = {
            "sha256": "a" * 64,
            "sha1": "b" * 40,
            "md5": "c" * 32,
            "decompiled_content": [],
            "decompiled_refs": [],
            "smali_content": [],
            "smali_refs": [],
            "similarity_metrics": [],
            "strings": strings_data,
            "analysis_errors": [],
        }
        return ext, apk_path

    def test_strings_table_present(self):
        ext, path = self._make_extractor_with_strings([
            {"string": "hello", "string_encoding": "UTF8",
             "string_offset": 0, "string_length": 5, "string_entropy": 2.32}
        ])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            assert "strings_raw" in export
        finally:
            os.unlink(path)

    def test_strings_table_schema(self):
        ext, path = self._make_extractor_with_strings([
            {"string": "hello", "string_encoding": "UTF8",
             "string_offset": 0, "string_length": 5, "string_entropy": 2.32}
        ])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            sr = export["strings_raw"]
            assert sr["table"] == "code_binja_strings_raw"
            assert len(sr["column_names"]) == 8
            assert len(sr["column_type_names"]) == 8
            assert "sha256" in sr["column_names"]
            assert "string" in sr["column_names"]
            assert "string_raw" in sr["column_names"]
            assert "string_encoding" in sr["column_names"]
            assert "string_offset" in sr["column_names"]
            assert "string_length" in sr["column_names"]
            assert "string_raw_length" in sr["column_names"]
            assert "string_entropy" in sr["column_names"]
            assert len(sr["data"]) == 1
            assert len(sr["data"][0]) == 8
        finally:
            os.unlink(path)

    def test_strings_sha256_propagated(self):
        ext, path = self._make_extractor_with_strings([
            {"string": "test", "string_encoding": "UTF8",
             "string_offset": 0, "string_length": 4, "string_entropy": 2.0}
        ])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            row = export["strings_raw"]["data"][0]
            assert row[0] == "a" * 64  # sha256 is first column
        finally:
            os.unlink(path)

    def test_empty_strings_not_exported(self):
        ext, path = self._make_extractor_with_strings([])
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            assert "strings_raw" not in export
        finally:
            os.unlink(path)


# ---------------------------------------------------------------------------
# Tests: IOCExtractorFromResults with APK format
# ---------------------------------------------------------------------------

class TestIOCExtractorAPKFormat:
    """Tests that IOCExtractorFromResults handles APK analysis_results."""

    def setup_method(self):
        self.log = MagicMock()

    def test_extract_iocs_from_apk_strings(self):
        """IOCs extracted from APK strings (same format as Binja)."""
        results = {
            "strings": [
                {"string": "https://evil.com/payload", "string_offset": 0},
                {"string": "Contact [email protected]", "string_offset": 1},
            ],
            "decompiled": [],
            "decompiled_content": [],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()

        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert len(url_iocs) >= 1
        assert any("evil.com" in i.ioc_value for i in url_iocs)

    def test_extract_iocs_from_apk_decompiled_content(self):
        """IOCs extracted from APK's decompiled_content key."""
        results = {
            "strings": [],
            "decompiled": [],  # Binja format — empty
            "decompiled_content": [  # APK format
                {
                    "decompiled_method": "connect('https://c2.malware.org/gate');",
                    "decompiled_method_hash": "hash123",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()

        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert len(url_iocs) >= 1

    def test_apk_library_methods_skipped(self):
        """LIBRARY methods in decompiled_content are skipped."""
        results = {
            "strings": [],
            "decompiled": [],
            "decompiled_content": [
                {
                    "decompiled_method": "https://should-skip.com",
                    "decompiled_method_hash": "lib_hash",
                    "method_type": "LIBRARY",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        assert len(iocs) == 0

    def test_combined_binja_and_apk_sources(self):
        """Both Binja and APK format decompiled entries are processed."""
        results = {
            "strings": [],
            "decompiled": [
                {
                    "decompiled_function": "call('https://binja.example.com');",
                    "decompiled_function_hash": "binja_hash",
                    "function_type": "USER",
                }
            ],
            "decompiled_content": [
                {
                    "decompiled_method": "send('https://apk.example.com');",
                    "decompiled_method_hash": "apk_hash",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()

        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        urls = [i.ioc_value for i in url_iocs]
        assert any("binja.example.com" in u for u in urls)
        assert any("apk.example.com" in u for u in urls)

    def test_apk_results_only(self):
        """Works when only APK keys are present (no 'decompiled' key)."""
        results = {
            "strings": [
                {"string": "https://evil.com", "string_offset": 0},
            ],
            "decompiled_content": [
                {
                    "decompiled_method": "x = 1",
                    "decompiled_method_hash": "h",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        # Should not crash, and should find URL from strings
        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert len(url_iocs) >= 1

    def test_source_type_is_string_for_strings(self):
        results = {
            "strings": [
                {"string": "https://evil.com/test", "string_offset": 42},
            ],
            "decompiled_content": [],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert url_iocs[0].source_type == SourceType.STRING
        assert url_iocs[0].source_identifier == "42"

    def test_source_type_is_decompiled_for_methods(self):
        results = {
            "strings": [],
            "decompiled_content": [
                {
                    "decompiled_method": "https://evil.com/method",
                    "decompiled_method_hash": "myhash",
                    "method_type": "USER",
                }
            ],
        }
        extractor = IOCExtractorFromResults(results, sha256="a" * 64, log=self.log)
        iocs = extractor.extract()
        url_iocs = [i for i in iocs if i.ioc_type == IOCType.URL]
        assert url_iocs[0].source_type == SourceType.DECOMPILED_FUNCTION
        assert url_iocs[0].source_identifier == "myhash"


# ---------------------------------------------------------------------------
# Tests: Worker IOC wiring for APK
# ---------------------------------------------------------------------------

class TestWorkerAPKIOCWiring:
    """Tests that workers.py wires IOC extraction for APK."""

    def test_worker_source_contains_ioc_for_apk(self):
        """Verify workers.py source has IOC extraction in APK branch."""
        import inspect
        from redb import workers
        source = inspect.getsource(workers)
        # APK branch should import IOCExtractorFromResults
        assert "IOCExtractorFromResults" in source
        # Should be in the APK section (near DecompileAPK)
        apk_section = source[source.index("DecompileAPK("):]
        ioc_pos = apk_section.find("IOCExtractorFromResults")
        assert ioc_pos != -1, "IOCExtractorFromResults not found after DecompileAPK"