Weimin Su

60 papers C 5Journal 45Unranked 10
YearRankTypeTitle / Venue / Authors
2025 J jnl
IEEE Trans. Geosci. Remote. Sens.
Shengqi Zhou, Xingyu Lu, Jianchao Yang, Huizhang Yang, Junpeng Du, Lunhao Duan, Wenchao Yu, Ke Tan, Shaojia Ge, Weimin Su, Hong Gu
2024 C conf
IGARSS
Ke Tan, Shengqi Zhou, Xingyu Lu, Jianchao Yang, Hong Gu, Weimin Su
2024 J jnl
Sensors
Benru Yu, Hong Gu, Weimin Su
2024 J jnl
Sensors
Guanqi Tong, Xingyu Lu, Jianchao Yang, Wenchao Yu, Hong Gu, Weimin Su
2024 J jnl
IEEE Geosci. Remote. Sens. Lett.
Shiyuan Zhang, Xingyu Lu, Jintao Yu, Zheng Dai, Weimin Su, Hong Gu
2024 J jnl
IEEE Trans. Aerosp. Electron. Syst.
Wenchao Yu, Xingyu Lu, Weimin Su, Hong Gu, Jianchao Yang
2024 J jnl
Sensors
Rui Li, Jianchao Yang, Zheng Dai, Xingyu Lu, Ke Tan, Weimin Su
2024 C conf
IGARSS
Shengqi Zhou, Guang Hu, Jingqiao Wang, Qi Chen, Junpeng Du, Xingyu Lu, Zhichuan Wang, Jianchao Yang, Ke Tan, Hong Gu, Weimin Su
2023 J jnl
IEEE Geosci. Remote. Sens. Lett.
Shaojia Ge, Hong Gu, Weimin Su, Anne Lönnqvist, Oleg Antropov
2023 J jnl
Signal Image Video Process.
Wenchao Yu, Weimin Su, Hong Gu, Jianchao Yang, Xingyu Lu
2023 J jnl
Sensors
Ke Tan, Shengqi Zhou, Xingyu Lu, Jianchao Yang, Weimin Su, Hong Gu
2023 J jnl
Remote. Sens.
Shaojia Ge, Erkki Tomppo, Yrjö Rauste, Ronald E. McRoberts, Jaan Praks, Hong Gu, Weimin Su, Oleg Antropov
2022 J jnl
CoRR
Shaojia Ge, Hong Gu, Weimin Su, Anne Lönnqvist, Oleg Antropov
2022 J jnl
IEEE Trans. Geosci. Remote. Sens.
Xingyu Lu, Jianchao Yang, Tat Soon Yeo, Weimin Su, Hong Gu, Wenchao Yu
2022 J jnl
IEEE Trans. Geosci. Remote. Sens.
Xingyu Lu, Chenchen Wang, Xiaofeng Xu, Huizhang Yang, Shiyuan Zhang, Ke Tan, Xianglin Bao, Weimin Su, Hong Gu
2022 J jnl
Digit. Signal Process.
Wenchao Yu, Xingyu Lu, Weimin Su, Hong Gu, Jianchao Yang
2022 conf
ICCAIS
Benru Yu, Hong Gu, Weimin Su, Tiancheng Li
2022 C conf
IGARSS
Shaojia Ge, Hong Gu, Weimin Su, Jaan Praks, Anne Lönnqvist, Oleg Antropov
2022 J jnl
Remote. Sens.
Shaojia Ge, Weimin Su, Hong Gu, Yrjö Rauste, Jaan Praks, Oleg Antropov
2022 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Shaojia Ge, Hong Gu, Weimin Su, Jaan Praks, Oleg Antropov
2022 J jnl
IEEE Geosci. Remote. Sens. Lett.
Chenchen Wang, Weimin Su, Hong Gu
2022 J jnl
IEEE Geosci. Remote. Sens. Lett.
Chenchen Wang, Hong Gu, Weimin Su
2021 J jnl
IEEE Commun. Lett.
Zheng Dai, Weimin Su, Hong Gu
2021 J jnl
Remote. Sens.
Ke Tan, Xingyu Lu, Jianchao Yang, Weimin Su, Hong Gu
2021 J jnl
IEEE Trans. Signal Process.
Xingyu Lu, Bangzhen Xu, Tat Soon Yeo, Weimin Su, Hong Gu
2021 J jnl
IEEE Commun. Lett.
Zheng Dai, Weimin Su, Hong Gu
2021 J jnl
IEEE Trans. Geosci. Remote. Sens.
Xingyu Lu, Jianchao Yang, Wenchao Yu, Weimin Su, Hong Gu, Tat Soon Yeo
2021 J jnl
IEEE Access
Yujiu Zhao, Yiqin Chen, Matthew Ritchie, Weimin Su, Hong Gu
2021 J jnl
IEICE Trans. Commun.
Zheng Dai, Weimin Su, Hong Gu
2020 J jnl
IEICE Trans. Fundam. Electron. Commun. Comput. Sci.
Zheng Dai, Weimin Su, Hong Gu
2020 J jnl
IEEE Geosci. Remote. Sens. Lett.
Xingyu Lu, Yujiu Zhao, Jianchao Yang, Weimin Su, Hong Gu, Tat Soon Yeo
2020 J jnl
Sensors
Zheng Dai, Weimin Su, Hong Gu
2020 C conf
IGARSS
Shaojia Ge, Erkki Tomppo, Yrjö Rauste, Weimin Su, Hong Gu, Jaan Praks, Oleg Antropov
2020 J jnl
IEEE Access
Wenjuan Li, Hong Gu, Weimin Su
2020 J jnl
Signal Process.
Wenchao Yu, Weimin Su, Hong Gu, Jianchao Yang, Xingyu Lu
2019 C conf
IGARSS
Shaojia Ge, Oleg Antropov, Weimin Su, Hong Gu, Jaan Praks
2019 J jnl
Digit. Signal Process.
Wenchao Yu, Weimin Su, Hong Gu, Jianchao Yang, Xingyu Lu
2019 J jnl
Sensors
Chenchen Wang, Weimin Su, Hong Gu, Jianchao Yang
2019 conf
ICCAIS
Xiaohu Zhang, Hong Gu, Weimin Su
2019 conf
ICCAIS
Bangzhen Xu, Yiqin Chen, Hong Gu, Weimin Su
2018 J jnl
Multidimens. Syst. Signal Process.
Wenchao Yu, Weimin Su, Hong Gu
2018 J jnl
Signal Process.
Wenchao Yu, Weimin Su, Hong Gu
2018 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Xingyu Lu, Weimin Su, Jianchao Yang, Hong Gu, Hailong Zhang, Wenchao Yu, Tat Soon Yeo
2017 J jnl
IEICE Trans. Fundam. Electron. Commun. Comput. Sci.
Zheng Dai, Weimin Su, Hong Gu
2017 J jnl
IEICE Trans. Fundam. Electron. Commun. Comput. Sci.
Zheng Dai, Weimin Su, Hong Gu
2017 J jnl
Digit. Signal Process.
Wenchao Yu, Weimin Su, Hong Gu
2017 conf
ICSPCC
Zhao Wang, Pengyi Wang, Weimin Su, Hong Gu
2017 conf
ICFSP
Shaojia Ge, Jianchun Lu, Hong Gu, Zeshi Yuan, Weimin Su
2016 conf
ICSPS
Zheng Dai, Weimin Su, Hong Gu
2016 J jnl
J. Electronic Imaging
Hongbin Zhang, Chang-Hong Fu, Yui-Lam Chan, Sik-Ho Tsang, Wan-Chi Siu, Weimin Su
2015 conf
DSP
Hongbin Zhang, Chang-Hong Fu, Weimin Su, Sik-Ho Tsang, Yui-Lam Chan
2015 conf
APSIPA
Hongbin Zhang, Sik-Ho Tsang, Yui-Lam Chan, Chang-Hong Fu, Weimin Su
2015 conf
DSP
Chang-Hong Fu, Hongbin Zhang, Weimin Su, Sik-Ho Tsang, Yui-Lam Chan
2015 J jnl
Theor. Comput. Sci.
Jianxin Wang, Weimin Su, Min Yang, Jiong Guo, Qilong Feng, Feng Shi, Jianer Chen
2015 J jnl
Multidimens. Syst. Signal Process.
Hua Shao, Weimin Su, Hong Gu, Jinyu Fan, Jinli Chen
2014 conf
DSP
Hongbin Zhang, Chang-Hong Fu, Weimin Su, Yui-Lam Chan
2013 J jnl
Signal Process.
Yuanbing Cheng, Rusheng Yu, Hong Gu, Weimin Su
2012 J jnl
IEEE Commun. Lett.
Yuanbing Cheng, Hong Gu, Weimin Su
2011 J jnl
Signal Process.
Limin Yang, Weimin Su, Hong Gu, Runtong Geng
2010 J jnl
Signal Process.
Jinli Chen, Hong Gu, Weimin Su
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"