R. Bhushan Gopaluni

73 papers A* 2A 1C 17Journal 46Unranked 7
YearRankTypeTitle / Venue / Authors
2026 J jnl
IEEE CAA J. Autom. Sinica
Liang Cao, Jianping Su, Fan Yang, Yankai Cao, R. Bhushan Gopaluni
2025 J jnl
Annu. Rev. Control.
Nathan P. Lawrence, Philip D. Loewen, Michael G. Forbes, R. Bhushan Gopaluni, Ali Mesbah
2025 J jnl
CoRR
Nathan P. Lawrence, Philip D. Loewen, Michael G. Forbes, R. Bhushan Gopaluni, Ali Mesbah
2025 J jnl
IEEE Trans. Instrum. Meas.
Liang Cao, Jingyi Wang, Jianping Su, Yi Luo, Yankai Cao, Richard D. Braatz, R. Bhushan Gopaluni
2025 A* conf
ICML
Shuyuan Wang, Philip D. Loewen, Michael G. Forbes, R. Bhushan Gopaluni, Wei Pan
2025 J jnl
CoRR
Shuyuan Wang, Philip D. Loewen, Michael G. Forbes, R. Bhushan Gopaluni, Wei Pan
2025 J jnl
Comput. Chem. Eng.
Michael Baldea, Apostolos T. Georgiou, R. Bhushan Gopaluni, Mehmet Mercangöz, Constantinos C. Pantelides, Kiran Sheth, Victor M. Zavala, Christos Georgakis
2025 J jnl
Eng. Appl. Artif. Intell.
Ibrahim Yousef, Sirish L. Shah, R. Bhushan Gopaluni
2024 conf
L4DC
Nathan P. Lawrence, Philip D. Loewen, Shuyuan Wang, Michael G. Forbes, R. Bhushan Gopaluni
2024 J jnl
CoRR
Nathan P. Lawrence, Philip D. Loewen, Shuyuan Wang, Michael G. Forbes, R. Bhushan Gopaluni
2024 J jnl
IEEE Trans. Ind. Electron.
Yupeng Li, Weihua Cao, R. Bhushan Gopaluni, Wenkai Hu, Chao Gan, Min Wu
2024 A conf
IROS
Shuyuan Wang, Jingliang Duan, Nathan P. Lawrence, Philip D. Loewen, Michael G. Forbes, R. Bhushan Gopaluni, Lixian Zhang
2024 J jnl
CoRR
Shuyuan Wang, Jingliang Duan, Nathan P. Lawrence, Philip D. Loewen, Michael G. Forbes, R. Bhushan Gopaluni, Lixian Zhang
2024 J jnl
CoRR
Nathan P. Lawrence, Seshu Kumar Damarla, Jong Woo Kim, Aditya Tulsyan, Faraz Amjad, Kai Wang, Benoît Chachuat, Jong Min Lee, Biao Huang, R. Bhushan Gopaluni
2024 J jnl
Comput. Chem. Eng.
Prodromos Daoutidis, Jay H. Lee, Srinivas Rangarajan, Leo Chiang, R. Bhushan Gopaluni, Artur M. Schweidtmann, Iiro Harjunkoski, Mehmet Mercangöz, Ali Mesbah, Fani Boukouvala, Fernando V. Lima, Ehecatl Antonio del Rio-Chanona, Christos Georgakis
2024 J jnl
Autom.
Nathan P. Lawrence, Philip D. Loewen, Shuyuan Wang, Michael G. Forbes, R. Bhushan Gopaluni
2024 conf
ICPS
Liang Cao, Yankai Cao, R. Bhushan Gopaluni
2023 J jnl
CoRR
Nathan P. Lawrence, Philip D. Loewen, Shuyuan Wang, Michael G. Forbes, R. Bhushan Gopaluni
2023 J jnl
CoRR
Tobi Michael Alabi, Nathan P. Lawrence, Lin Lu, Zaiyue Yang, R. Bhushan Gopaluni
2023 J jnl
IEEE Trans. Ind. Informatics
Atefeh Daemi, R. Bhushan Gopaluni, Biao Huang
2023 J jnl
CoRR
Shuyuan Wang, Philip D. Loewen, Nathan P. Lawrence, Michael G. Forbes, R. Bhushan Gopaluni
2023 J jnl
Comput. Chem. Eng.
Jingyi Wang, Jesús Moreira, Yankai Cao, R. Bhushan Gopaluni
2023 J jnl
CoRR
Nathan P. Lawrence, Philip D. Loewen, Shuyuan Wang, Michael G. Forbes, R. Bhushan Gopaluni
2022 J jnl
CoRR
Lim C. Siang, Shams Elnawawi, Lee D. Rippon, Daniel L. O'Connor, R. Bhushan Gopaluni
2022 J jnl
CoRR
Daniel G. McClement, Nathan P. Lawrence, Johan U. Backström, Philip D. Loewen, Michael G. Forbes, R. Bhushan Gopaluni
2022 J jnl
CoRR
Daniel G. McClement, Nathan P. Lawrence, Michael G. Forbes, Philip D. Loewen, Johan U. Backström, R. Bhushan Gopaluni
2022 J jnl
CoRR
R. Bhushan Gopaluni, Aditya Tulsyan, Benoît Chachuat, Biao Huang, Jong Min Lee, Faraz Amjad, Seshu Kumar Damarla, Jong Woo Kim, Nathan P. Lawrence
2022 conf
SICE
Liang Cao, R. Bhushan Gopaluni, Lim C. Siang, Yankai Cao, Jin Li
2022 C conf
ACC
Jingyi Wang, Jesús Moreira, Yankai Cao, R. Bhushan Gopaluni
2021 J jnl
CoRR
Daniel G. McClement, Nathan P. Lawrence, Philip D. Loewen, Michael G. Forbes, Johan U. Backström, R. Bhushan Gopaluni
2021 J jnl
CoRR
Nathan P. Lawrence, Philip D. Loewen, Michael G. Forbes, Johan U. Backström, R. Bhushan Gopaluni
2021 J jnl
CoRR
Nathan P. Lawrence, Michael G. Forbes, Philip D. Loewen, Daniel G. McClement, Johan U. Backström, R. Bhushan Gopaluni
2021 J jnl
Comput. Chem. Eng.
Lee D. Rippon, Ibrahim Yousef, Behrooz Hosseini, Arbi Bouchoucha, Jean-Francois Beaulieu, Carole Prévost, Michel Ruel, S. L. Shah, R. Bhushan Gopaluni
2020 J jnl
Comput. Chem. Eng.
Hui Tian, Jagadeesan Prakash, Victor M. Zavala, James A. Olson, R. Bhushan Gopaluni
2020 A* conf
NeurIPS
Nathan P. Lawrence, Philip D. Loewen, Michael G. Forbes, Johan U. Backström, R. Bhushan Gopaluni
2020 J jnl
IEEE Trans. Ind. Informatics
Kai Wang, R. Bhushan Gopaluni, Junghui Chen, Zhihuan Song
2020 J jnl
CoRR
Steven Spielberg, Aditya Tulsyan, Nathan P. Lawrence, Philip D. Loewen, R. Bhushan Gopaluni
2020 J jnl
CoRR
Nathan P. Lawrence, Gregory E. Stewart, Philip D. Loewen, Michael G. Forbes, Johan U. Backström, R. Bhushan Gopaluni
2020 J jnl
CoRR
Nathan P. Lawrence, Gregory E. Stewart, Philip D. Loewen, Michael G. Forbes, Johan U. Backström, R. Bhushan Gopaluni
2020 J jnl
Comput. Chem. Eng.
Qiugang Lu, Michael G. Forbes, Philip D. Loewen, Johan U. Backström, Guy Albert Dumont, R. Bhushan Gopaluni
2019 J jnl
Autom.
Qiugang Lu, Philip D. Loewen, R. Bhushan Gopaluni, Michael G. Forbes, Johan U. Backström, Guy Albert Dumont, Michael S. Davies
2019 conf
IEEE SENSORS
Bryan Bohn, James A. Olson, R. Bhushan Gopaluni, Boris Stoeber
2019 J jnl
IEEE Access
Kai Wang, Michael G. Forbes, R. Bhushan Gopaluni, Junghui Chen, Zhihuan Song
2018 J jnl
Signal Process.
Aditya Tulsyan, Swanand R. Khare, Biao Huang, R. Bhushan Gopaluni, J. Fraser Forbes
2018 J jnl
Comput. Chem. Eng.
Mahsa Sadeghassadi, Chris J. B. Macnab, R. Bhushan Gopaluni, David T. Westwick
2018 J jnl
Comput. Chem. Eng.
Qiugang Lu, Benben Jiang, R. Bhushan Gopaluni, Philip D. Loewen, Richard D. Braatz
2017 J jnl
Comput. Chem. Eng.
David S. Zamar, R. Bhushan Gopaluni, Shahab Sokhansanj, Nathaniel K. Newlands
2017 conf
ASCC
Koji Kitano, Manabu Kano, R. Bhushan Gopaluni
2017 C conf
ACC
Qiugang Lu, Lee D. Rippon, R. Bhushan Gopaluni, Michael G. Forbes, Philip D. Loewen, Johan U. Backström, Guy Albert Dumont
2017 J jnl
IEEE Trans. Control. Syst. Technol.
Qiugang Lu, Michael G. Forbes, R. Bhushan Gopaluni, Philip D. Loewen, Johan U. Backström, Guy Albert Dumont
2016 J jnl
Medical Biol. Eng. Comput.
O. Vahidi, K. E. Kwok, R. Bhushan Gopaluni, F. K. Knop
2016 C conf
ACC
Hui Tian, Qiugang Lu, R. Bhushan Gopaluni, Victor M. Zavala, James A. Olson
2016 C conf
ACC
Melissa Barazandegan, K. Ezra Kwok, R. Bhushan Gopaluni
2016 conf
CDC
Hui Tian, Qiugang Lu, R. Bhushan Gopaluni, Victor M. Zavala
2016 J jnl
Comput. Chem. Eng.
Aditya Tulsyan, R. Bhushan Gopaluni, Swanand R. Khare
2016 J jnl
Comput. Chem. Eng.
Hong Jang, Kwang-Ki K. Kim, Richard D. Braatz, R. Bhushan Gopaluni, Jay H. Lee
2016 C conf
ACC
Aditya Tulsyan, R. Bhushan Gopaluni
2015 C conf
ACC
Qiugang Lu, Lee D. Rippon, R. Bhushan Gopaluni, Michael G. Forbes, Philip D. Loewen, Johan U. Backström, Guy Albert Dumont
2015 C conf
ACC
Mahdi Yousefi, Qiugang Lu, R. Bhushan Gopaluni, Philip D. Loewen, Michael G. Forbes, Guy Albert Dumont, Johan U. Backström
2015 C conf
ACC
Marcello Torchio, Nicolas A. Wolff, Davide Martino Raimondo, Lalo Magni, Ulrike Krewer, R. Bhushan Gopaluni, Joel A. Paulson, Richard D. Braatz
2014 C conf
CCA
Mahdi Yousefi, Michael G. Forbes, R. Bhushan Gopaluni, Philip D. Loewen, Guy Albert Dumont, Johan U. Backström
2014 C conf
ACC
Mahdi Yousefi, Michael G. Forbes, R. Bhushan Gopaluni, Guy Albert Dumont, Johan U. Backström, Ashish Malhotra
2014 C conf
ACC
Kwang Ki Kevin Kim, Hong Jang, R. Bhushan Gopaluni, Jay H. Lee, Richard D. Braatz
2013 J jnl
IEEE Trans. Aerosp. Electron. Syst.
Aditya Tulsyan, Biao Huang, R. Bhushan Gopaluni, J. Fraser Forbes
2013 C conf
ACC
Vinay A. Bavdekar, Jagdeesan Prakash, Sirish L. Shah, R. Bhushan Gopaluni
2013 C conf
ACC
Bharatkumar Suthar, Venkatasailanathan Ramadesigan, Paul W. C. Northrop, R. Bhushan Gopaluni, Shriram Santhanagopalan, Richard D. Braatz, Venkat R. Subramanian
2011 C conf
ACC
O. Vahidi, R. Bhushan Gopaluni, K. E. Kwok
2010 C conf
ACC
O. Vahidi, K. E. Kwok, R. Bhushan Gopaluni, L. Sun
2010 C conf
ACC
Devin J. Marshman, Terrance Chmelyk, M. S. Sidhu, R. Bhushan Gopaluni, Guy Albert Dumont
2008 J jnl
Inf. Sci.
Rogemar S. Mamon, Christina Erlwein, R. Bhushan Gopaluni
2006 conf
CDC
R. Bhushan Gopaluni, Philip D. Loewen, Mohammed Ammar, Guy Albert Dumont, Michael S. Davies
2003 J jnl
Autom.
R. Bhushan Gopaluni, R. S. Patwardhan, S. L. Shah
2002 C conf
ACC
R. Bhushan Gopaluni, R. S. Patwardhan, Sirish L. Shah
redb/extractors/js_extractors/js_patterns.py
← Index redb/extractors/js_extractors/js_patterns.py python
"""Canonical, compiled JavaScript regex patterns shared across JS extractors.

All suspicious-API patterns and the few feature-only patterns live here so each
expression is compiled exactly once per Python process and so any pattern that
was previously duplicated across `js_features.py` and `js_suspicious_apis.py`
now resolves to a single shared compiled object.

JavaScript is case-sensitive at runtime, but every suspicious-API pattern matches
either a literal-case identifier (`\\beval\\s*\\(`, `String\\.fromCharCode`, etc.)
or a string-quoted token (`"powershell"`). Compiling them with `re.IGNORECASE`
matches the historical behaviour of `JSSuspiciousAPIsExtractor` and is safe for
the patterns that historically came from `JSFeaturesExtractor` — those literals
are spelled in real-world JS exactly as written.

`scan_source()` is the entry point used by extractors: it walks the source once
per pattern using the pre-compiled regexes and returns a flat
`{name: {"count": N, "lines": [unique_line_numbers_sorted]}}` dict. Both
`JSFeaturesExtractor` and `JSSuspiciousAPIsExtractor` consume the same dict so
the per-pattern × per-line loops they used to run independently collapse to a
single shared scan.
"""

import bisect
import re
from typing import Dict, Iterable, List, Mapping

_FLAGS = re.IGNORECASE

# Canonical compiled patterns, keyed by their human-readable name. The name is
# also the value emitted into `redb_js_suspicious_apis.api_name`.
PATTERNS = {
    # ---- code execution ----
    "eval": re.compile(r"\beval\s*\(", _FLAGS),
    "Function constructor": re.compile(r"\bnew\s+Function\s*\(", _FLAGS),
    "execScript": re.compile(r"\bexecScript\s*\(", _FLAGS),
    "document.write": re.compile(r"\bdocument\.write(?:ln)?\s*\(", _FLAGS),
    "innerHTML assignment": re.compile(r"\.innerHTML\s*=", _FLAGS),
    "outerHTML assignment": re.compile(r"\.outerHTML\s*=", _FLAGS),
    "insertAdjacentHTML": re.compile(r"\.insertAdjacentHTML\s*\(", _FLAGS),
    # ---- network ----
    "XMLHttpRequest": re.compile(r"\bnew\s+XMLHttpRequest\b", _FLAGS),
    "fetch": re.compile(r"\bfetch\s*\(", _FLAGS),
    "WebSocket": re.compile(r"\bnew\s+WebSocket\s*\(", _FLAGS),
    "navigator.sendBeacon": re.compile(r"\bnavigator\.sendBeacon\s*\(", _FLAGS),
    "ActiveXObject XMLHTTP": re.compile(
        r"ActiveXObject\s*\(\s*[\"\'](?:MSXML2\.XMLHTTP|Microsoft\.XMLHTTP)", _FLAGS
    ),
    "require network module": re.compile(
        r"require\s*\(\s*[\"\'](?:http|https|net|dgram)[\"\']", _FLAGS
    ),
    "axios": re.compile(r"\baxios\b", _FLAGS),
    # ---- filesystem ----
    "require fs": re.compile(r"require\s*\(\s*[\"\']fs[\"\']", _FLAGS),
    "require path": re.compile(r"require\s*\(\s*[\"\']path[\"\']", _FLAGS),
    "FileSystemObject": re.compile(r"Scripting\.FileSystemObject", _FLAGS),
    "ADODB.Stream": re.compile(r"ADODB\.Stream", _FLAGS),
    "Shell.Application": re.compile(r"Shell\.Application", _FLAGS),
    "WScript.CreateObject": re.compile(r"WScript\.CreateObject", _FLAGS),
    # ---- process ----
    "require child_process": re.compile(r"require\s*\(\s*[\"\']child_process[\"\']", _FLAGS),
    "child_process exec": re.compile(r"child_process\.(?:exec|spawn|execFile|fork)\s*\(", _FLAGS),
    "WScript.Shell": re.compile(r"WScript\.Shell", _FLAGS),
    "WScript.Shell.Run": re.compile(r"\.Run\s*\(", _FLAGS),
    "WScript.Shell.Exec": re.compile(r"\.Exec\s*\(", _FLAGS),
    "ShellExecute": re.compile(r"\bShellExecute\b", _FLAGS),
    "PowerShell reference": re.compile(r"[\"\']powershell[\"\']", _FLAGS),
    "cmd.exe reference": re.compile(r"[\"\']cmd\.exe[\"\']", _FLAGS),
    "require os": re.compile(r"require\s*\(\s*[\"\']os[\"\']", _FLAGS),
    # ---- registry ----
    "RegRead": re.compile(r"\.RegRead\s*\(", _FLAGS),
    "RegWrite": re.compile(r"\.RegWrite\s*\(", _FLAGS),
    "RegDelete": re.compile(r"\.RegDelete\s*\(", _FLAGS),
    "StdRegProv": re.compile(r"StdRegProv", _FLAGS),
    # ---- crypto / encoding ----
    "atob": re.compile(r"\batob\s*\(", _FLAGS),
    "btoa": re.compile(r"\bbtoa\s*\(", _FLAGS),
    "String.fromCharCode": re.compile(r"String\.fromCharCode\s*\(", _FLAGS),
    "unescape": re.compile(r"\bunescape\s*\(", _FLAGS),
    "decodeURIComponent": re.compile(r"\bdecodeURIComponent\s*\(", _FLAGS),
    "Buffer.from": re.compile(r"Buffer\.from\s*\(", _FLAGS),
    "crypto module": re.compile(r"crypto\.create(?:Cipher|Decipher|Hash|Hmac)", _FLAGS),
    # ---- DOM manipulation ----
    "document.forms": re.compile(r"document\.forms", _FLAGS),
    "document.cookie": re.compile(r"document\.cookie", _FLAGS),
    "querySelector sensitive input": re.compile(
        r"document\.querySelector\s*\([^)]*(?:password|credit|card|cvv|ssn)", _FLAGS
    ),
    "submit event listener": re.compile(r"addEventListener\s*\(\s*[\"\']submit", _FLAGS),
    "createElement script/iframe": re.compile(
        r"\.createElement\s*\(\s*[\"\'](?:script|iframe)", _FLAGS
    ),
    "dynamic script src": re.compile(r"\.src\s*=\s*[\"\'](?:https?://|//)", _FLAGS),
}

# Pattern name -> category (one of code_execution / network / filesystem /
# process / registry / crypto_encoding / dom_manipulation).
CATEGORIES = {
    "eval": "code_execution",
    "Function constructor": "code_execution",
    "execScript": "code_execution",
    "document.write": "code_execution",
    "innerHTML assignment": "code_execution",
    "outerHTML assignment": "code_execution",
    "insertAdjacentHTML": "code_execution",
    "XMLHttpRequest": "network",
    "fetch": "network",
    "WebSocket": "network",
    "navigator.sendBeacon": "network",
    "ActiveXObject XMLHTTP": "network",
    "require network module": "network",
    "axios": "network",
    "require fs": "filesystem",
    "require path": "filesystem",
    "FileSystemObject": "filesystem",
    "ADODB.Stream": "filesystem",
    "Shell.Application": "filesystem",
    "WScript.CreateObject": "filesystem",
    "require child_process": "process",
    "child_process exec": "process",
    "WScript.Shell": "process",
    "WScript.Shell.Run": "process",
    "WScript.Shell.Exec": "process",
    "ShellExecute": "process",
    "PowerShell reference": "process",
    "cmd.exe reference": "process",
    "require os": "process",
    "RegRead": "registry",
    "RegWrite": "registry",
    "RegDelete": "registry",
    "StdRegProv": "registry",
    "atob": "crypto_encoding",
    "btoa": "crypto_encoding",
    "String.fromCharCode": "crypto_encoding",
    "unescape": "crypto_encoding",
    "decodeURIComponent": "crypto_encoding",
    "Buffer.from": "crypto_encoding",
    "crypto module": "crypto_encoding",
    "document.forms": "dom_manipulation",
    "document.cookie": "dom_manipulation",
    "querySelector sensitive input": "dom_manipulation",
    "submit event listener": "dom_manipulation",
    "createElement script/iframe": "dom_manipulation",
    "dynamic script src": "dom_manipulation",
}

# Patterns consumed only by JSFeaturesExtractor (no category, never surfaced as
# a suspicious-API row). Kept here so every JS regex is compiled in one place.
FEATURE_PATTERNS = {
    "hex_escape": re.compile(r"\\x[0-9a-fA-F]{2}"),
    "unicode_escape": re.compile(r"\\u[0-9a-fA-F]{4}"),
    "base64_string": re.compile(r"[A-Za-z0-9+/]{40,}={0,2}"),
    # decodeURI matches BOTH decodeURI and decodeURIComponent. The latter is also
    # a suspicious-API pattern in PATTERNS; this broader form is what the
    # `decodeuri_count` feature column has historically counted.
    "decodeURI": re.compile(r"\b(?:decodeURI|decodeURIComponent)\s*\(", _FLAGS),
    "settimeout_setinterval": re.compile(r"\b(?:setTimeout|setInterval)\s*\(", _FLAGS),
    "function_decl": re.compile(r"\bfunction\s+\w+\s*\(|\bfunction\s*\("),
    "var_decl": re.compile(r"\b(?:var|let|const)\s+"),
    "string_concat": re.compile(r"[\"\'][\s]*\+[\s]*[\"\']"),
    "comment": re.compile(r"//.*?$|/\*[\s\S]*?\*/", re.MULTILINE),
    "long_string": re.compile(r"[\"\']([^\"\']{256,})[\"\']"),
    "array_function_call": re.compile(r"\[(?:0x[0-9a-f]+|[\d]+)\]\s*\(", _FLAGS),
}

# Patterns consumed only by JSStringsExtractor for encoded-string discovery.
# Scoped to *hidden* strings only — patterns whose decoded form is not visible
# to a substring search over the raw text. Plain long literals are not
# extracted here because they're already preserved in code_text_content and
# scraped by the IOC pipeline over text_raw / text_normalized.
#
# Distinct from FEATURE_PATTERNS even where the names rhyme:
#   FEATURE_PATTERNS["hex_escape"] / ["unicode_escape"]   -> single escape
#   STRING_PATTERNS["hex_escape_seq"] / ["unicode_escape_seq"] -> 4+ / 3+ in a row
#   FEATURE_PATTERNS["base64_string"]                     -> bare base64 token
#   STRING_PATTERNS["base64_quoted"]                      -> base64 inside JS quotes
# These do not share match objects with the suspicious-API or feature scans, so
# they are not folded into JSContext.scan; the strings extractor walks them
# itself (one finditer per pattern, with shared line-offset bisect in #4b).
STRING_PATTERNS = {
    "hex_escape_seq": re.compile(r"(?:\\x[0-9a-fA-F]{2}){4,}"),
    "unicode_escape_seq": re.compile(r"(?:\\u[0-9a-fA-F]{4}){3,}"),
    "charcode_call": re.compile(r"String\.fromCharCode\s*\(\s*([\d,\s]+)\s*\)"),
    "base64_quoted": re.compile(r"[\"\']([A-Za-z0-9+/]{40,}={0,2})[\"\']"),
    "concat_chain": re.compile(r"(?:[\"\'][^\"\']+[\"\']\s*\+\s*){3,}[\"\'][^\"\']+[\"\']"),
}


def line_offsets(source: str) -> List[int]:
    """Sorted list of byte offsets for every newline in `source`, plus a final
    sentinel of len(source). Used to translate match offsets into 1-indexed
    line numbers via bisect.
    """
    offsets = [-1]  # so that bisect_right of offset 0 returns line 1
    push = offsets.append
    idx = source.find("\n")
    while idx != -1:
        push(idx)
        idx = source.find("\n", idx + 1)
    return offsets


def _scan_one(
    pattern: "re.Pattern[str]", source: str, offsets: List[int]
) -> Dict[str, object]:
    """Run a single compiled pattern over `source` and return count + unique lines."""
    count = 0
    seen_lines: "set[int]" = set()
    for m in pattern.finditer(source):
        count += 1
        seen_lines.add(bisect.bisect_right(offsets, m.start()))
    if not count:
        return None  # type: ignore[return-value]
    return {"count": count, "lines": sorted(seen_lines)}


def scan_source(
    source: str,
    patterns: Iterable[Mapping[str, "re.Pattern[str]"]] = (PATTERNS, FEATURE_PATTERNS),
) -> Dict[str, Dict[str, object]]:
    """Scan `source` against every compiled pattern in `patterns`.

    Returns a dict keyed by pattern name. Each entry has:
        "count": total number of matches in the source
        "lines": sorted list of unique 1-indexed line numbers where the pattern
                 matched (deduplicated — multiple matches on the same line
                 collapse to one entry, preserving the historical
                 line-set semantics of JSSuspiciousAPIsExtractor)
    Patterns with zero matches are absent from the dict; callers should default
    to {"count": 0, "lines": []}.
    """
    if not source:
        return {}
    offsets = line_offsets(source)
    results: Dict[str, Dict[str, object]] = {}
    for table in patterns:
        for name, pat in table.items():
            entry = _scan_one(pat, source, offsets)
            if entry is not None:
                results[name] = entry
    return results