Compare commits

..
Author SHA1 Message Date
Donncha Ó Cearbhaill 06cea62a40 Record wall time and peak memory growth per module
Command.run() times every module and measures how much the process's
peak resident set size grew while it ran. Both are logged at INFO in
one line per module and written to info.json under module_stats, in
run order, so any run says which module cost what and CI can compare a
branch against main.

ru_maxrss is the process peak, so a module's growth is how much it
raised the high-water mark, which is what matters on a host without
swap; a module running after a bigger one reports 0. Where the resource
module is unavailable the growth is reported as 0.
2026-09-16 00:45:47 +02:00
39 changed files with 102 additions and 197 deletions

No files matched your search

+1 -15
View File
@@ -43,25 +43,11 @@ class DumpsysAppopsArtifact(AndroidArtifact):
def check_indicators(self) -> None:
for result in self.results:
# A package IOC match is not tied to one AppOps entry. Use the latest
# recorded access or rejection to place the package-level alert in time.
event_time = max(
(
entry["timestamp"]
for permission in result["permissions"]
for entry in permission.get("entries", [])
if entry.get("timestamp")
),
default="",
)
if self.indicators:
ioc_match = self.indicators.check_app_id(result.get("package_name"))
if ioc_match:
self.alertstore.critical(
ioc_match.message,
event_time,
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -43,10 +43,7 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("period_start") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -24,10 +24,7 @@ class DumpsysBatteryHistoryArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(package_name)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
@staticmethod
+1 -4
View File
@@ -23,10 +23,7 @@ class DumpsysDBInfoArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(part)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -35,7 +35,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
alerted_root_packages.add(package_name)
self.alertstore.medium(
f'Found an installed package related to rooting/jailbreaking: "{package_name}"',
result.get("timestamp") or "",
"",
result,
)
if not self.indicators:
@@ -43,10 +43,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(package_name)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
@@ -99,10 +99,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
ioc_match = self.indicators.check_process(result["process_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -111,10 +108,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
ioc_match = self.indicators.check_process(command_name)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -129,7 +123,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
f"Potentially suspicious crash in process '{result['process_name']}' "
f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}"
),
result.get("timestamp") or "",
"",
result,
)
@@ -90,10 +90,7 @@ class AQFFiles(AndroidQFModule):
ioc_match = self.indicators.check_file_path(result["path"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("modified_time") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -108,7 +105,7 @@ class AQFFiles(AndroidQFModule):
file_type = "executable "
msg = f'Found {file_type}file at suspicious path "{result["path"]}"'
self.alertstore.high(msg, result.get("modified_time") or "", result)
self.alertstore.high(msg, "", result)
for hash_key in ("sha256", "sha1", "md5"):
file_hash = result.get(hash_key, "")
@@ -117,10 +114,7 @@ class AQFFiles(AndroidQFModule):
ioc_match = self.indicators.check_file_hash(file_hash)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("modified_time") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
break
+1 -4
View File
@@ -54,10 +54,7 @@ class SMS(BackupModule):
):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
message.get("isodate") or "",
message,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", message, matched_indicator=ioc_match.ioc
)
def collect_url_results(self) -> None:
+31
View File
@@ -7,6 +7,7 @@ import json
import logging
import os
import sys
import time
from datetime import datetime
from functools import cached_property
from heapq import heappop, heappush
@@ -36,6 +37,17 @@ from .utils import (
from .version import MVT_VERSION
def _peak_rss_mb() -> float:
"""The process's peak resident set size so far, in MB (0 where unknown)."""
try:
import resource
except ImportError:
return 0.0
peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
# ru_maxrss is in bytes on macOS and in kilobytes elsewhere.
return peak / (1024 * 1024 if sys.platform == "darwin" else 1024)
class Command:
def __init__(
self,
@@ -77,6 +89,9 @@ class Command:
# declaring `replaces` to the module which took their place.
self.module_replacements: dict[type[MVTModule], type[MVTModule]] = {}
# Wall time and peak-RSS growth of every module run, in run order.
self.module_stats: list[dict[str, Any]] = []
# This list will contain all executed modules.
# We can use this to reference e.g. self.executed[0].results.
self.executed: list[MVTModule] = []
@@ -190,6 +205,7 @@ class Command:
"date": convert_datetime_to_iso(datetime.now()),
"ioc_files": [],
"hashes": [],
"module_stats": self.module_stats,
}
for coll in self.iocs.ioc_collections:
@@ -766,6 +782,8 @@ class Command:
except NotImplementedError:
pass
started = time.perf_counter()
peak_before = _peak_rss_mb()
try:
run_module(m)
except EncryptedBackupError:
@@ -775,6 +793,19 @@ class Command:
)
return
stats = {
"module": module.__name__,
"seconds": round(time.perf_counter() - started, 2),
"peak_rss_growth_mb": round(_peak_rss_mb() - peak_before),
}
self.module_stats.append(stats)
self.log.info(
"Module %s ran in %.1fs and raised the peak memory by %d MB",
stats["module"],
stats["seconds"],
stats["peak_rss_growth_mb"],
)
self.executed.append(m)
executed_by_type[module] = m
self.timeline.extend(m.timeline)
-4
View File
@@ -1271,9 +1271,5 @@
{
"version": "26.6.1",
"build": "23G83"
},
{
"version": "26.6.2",
"build": "23G90"
}
]
@@ -74,10 +74,7 @@ class ConfigurationProfiles(IOSExtraction):
if ioc_match:
warning_message = f'Found a known malicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with UUID "{result["plist"]["PayloadUUID"]}"'
self.alertstore.critical(
warning_message,
result.get("install_date") or "",
result,
matched_indicator=ioc_match.ioc,
warning_message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -85,9 +82,7 @@ class ConfigurationProfiles(IOSExtraction):
# to hide notifications.
if payload_content["PayloadType"] in ["com.apple.notificationsettings"]:
warning_message = f'Found a potentially suspicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with payload type {payload_content["PayloadType"]}'
self.alertstore.medium(
warning_message, result.get("install_date") or "", result
)
self.alertstore.medium(warning_message, "", result)
continue
@staticmethod
+2 -5
View File
@@ -104,10 +104,7 @@ class Manifest(IOSExtraction):
ioc_match = self.indicators.check_file_path("/" + result["relative_path"])
if ioc_match:
self.alertstore.high(
ioc_match.message,
result.get("modified") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -123,7 +120,7 @@ class Manifest(IOSExtraction):
if ioc_match:
self.alertstore.high(
f'Found mention of domain "{ioc_match.ioc.value}" in a backup file with path: {rel_path}',
result.get("modified") or "",
"",
result,
matched_indicator=ioc_match.ioc,
)
+2 -8
View File
@@ -67,20 +67,14 @@ class ProfileEvents(IOSExtraction):
ioc_match = self.indicators.check_process(result.get("process") or "")
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
ioc_match = self.indicators.check_profile(result.get("profile_id") or "")
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
@staticmethod
+2 -5
View File
@@ -68,10 +68,7 @@ class Analytics(IOSExtraction):
warning_message = f'Found mention of a malicious process "{value}" in {result["artifact"]} file at {result["isodate"]}'
new_result = copy.copy(result)
self.alertstore.critical(
warning_message,
result.get("isodate") or "",
new_result,
matched_indicator=ioc_match.ioc,
warning_message, "", new_result, matched_indicator=ioc_match.ioc
)
continue
@@ -80,7 +77,7 @@ class Analytics(IOSExtraction):
new_result = copy.copy(result)
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
"",
new_result,
matched_indicator=ioc_match.ioc,
)
+2 -8
View File
@@ -59,10 +59,7 @@ class Filesystem(IOSExtraction):
ioc_match = self.indicators.check_file_path(result["path"])
if ioc_match:
self.alertstore.high(
ioc_match.message,
result.get("modified") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
# If we are instructed to run fast, we skip the rest.
@@ -72,10 +69,7 @@ class Filesystem(IOSExtraction):
ioc_match = self.indicators.check_file_path_process(result["path"])
if ioc_match:
self.alertstore.high(
ioc_match.message,
result.get("modified") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+1 -4
View File
@@ -63,10 +63,7 @@ class SafariFavicon(IOSExtraction):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def _process_favicon_db(self, file_path):
+2 -5
View File
@@ -60,10 +60,7 @@ class ShutdownLog(IOSExtraction):
ioc_match = self.indicators.check_file_path(result["client"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -72,7 +69,7 @@ class ShutdownLog(IOSExtraction):
if ioc.value in parts:
self.alertstore.critical(
f'Found mention of a known malicious process "{ioc.value}" in shutdown.log',
result.get("isodate") or "",
"",
result,
matched_indicator=ioc,
)
+1 -4
View File
@@ -21,10 +21,7 @@ class WebkitBase(IOSExtraction):
ioc_match = self.indicators.check_url(result["url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
+4 -4
View File
@@ -68,7 +68,7 @@ class Applications(IOSExtraction):
if "softwareVersionBundleId" not in result:
self.alertstore.medium(
"Suspicious application identified without softwareVersionBundleId",
result.get("isodate") or "",
"",
result,
)
continue
@@ -79,7 +79,7 @@ class Applications(IOSExtraction):
if ioc_match:
self.alertstore.critical(
f"Malicious application {result['softwareVersionBundleId']} identified",
result.get("isodate") or "",
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -91,7 +91,7 @@ class Applications(IOSExtraction):
if ioc_match:
self.alertstore.critical(
f"Malicious application {result['softwareVersionBundleId']} identified",
result.get("isodate") or "",
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -104,7 +104,7 @@ class Applications(IOSExtraction):
):
self.alertstore.medium(
f"Suspicious app not installed from the App Store or MDM: {result['softwareVersionBundleId']}",
result.get("isodate") or "",
"",
result,
)
+2 -5
View File
@@ -72,10 +72,7 @@ class Calendar(IOSExtraction):
ioc_match = self.indicators.check_email(result["participant_email"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("start_date") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -83,7 +80,7 @@ class Calendar(IOSExtraction):
if result["summary"] == "Meeting" and result["description"] == "Notes":
self.alertstore.high(
f"Potential Quadream exploit event identified: {result['uuid']}",
result.get("start_date") or "",
"",
result,
)
+1 -4
View File
@@ -61,10 +61,7 @@ class ChromeFavicon(IOSExtraction):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
+1 -4
View File
@@ -62,10 +62,7 @@ class ChromeHistory(IOSExtraction):
ioc_match = self.indicators.check_url(result["url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+1 -4
View File
@@ -64,10 +64,7 @@ class FirefoxFavicon(IOSExtraction):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+1 -4
View File
@@ -64,10 +64,7 @@ class FirefoxHistory(IOSExtraction):
ioc_match = self.indicators.check_url(result["url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+2 -5
View File
@@ -66,17 +66,14 @@ class IDStatusCache(IOSExtraction):
ioc_match = self.indicators.check_email(email)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
if "\\x00\\x00" in result.get("user", ""):
self.alertstore.high(
f"Found an ID Status Cache entry with suspicious patterns: {result.get('user')}",
result.get("isodate") or "",
"",
result,
)
+5 -13
View File
@@ -79,14 +79,6 @@ class LocationdClients(IOSExtraction):
return
for result in self.results:
event_time = next(
(
result[timestamp]
for timestamp in self.timestamps
if result.get(timestamp)
),
"",
)
parts = result["package"].split("/")
proc_name = parts[len(parts) - 1]
@@ -94,7 +86,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a suspicious process name in LocationD entry {result['package']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -105,7 +97,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a suspicious process name in LocationD entry {result['package']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -115,7 +107,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a known malicious domain in LocationD entry {result['package']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -126,7 +118,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a suspicious file path in LocationD entry {result['Executable']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -141,7 +133,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a suspicious file path in LocationD entry {result['Registered']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -65,10 +65,7 @@ class OSAnalyticsADDaily(IOSExtraction):
ioc_match = self.indicators.check_process(result["package"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("ts") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
@@ -73,10 +73,7 @@ class SafariBrowserState(IOSExtraction):
ioc_match = self.indicators.check_url(result["tab_url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("last_viewed_timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -89,7 +86,7 @@ class SafariBrowserState(IOSExtraction):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("last_viewed_timestamp") or "",
"",
result,
matched_indicator=ioc_match.ioc,
)
+1 -4
View File
@@ -126,10 +126,7 @@ class SafariHistory(IOSExtraction):
ioc_match = self.indicators.check_url(result["url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def _process_history_db(self, history_path):
+1 -4
View File
@@ -82,10 +82,7 @@ class Shortcuts(IOSExtraction):
):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+1 -4
View File
@@ -98,10 +98,7 @@ class SMS(IOSExtraction):
):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def collect_url_results(self) -> None:
+1 -1
View File
@@ -67,7 +67,7 @@ class SMSAttachments(IOSExtraction):
if ioc_match:
self.alertstore.high(
ioc_match.message,
attachment.get("isodate") or "",
"",
attachment,
matched_indicator=ioc_match.ioc,
)
+1 -4
View File
@@ -97,10 +97,7 @@ class TCC(IOSExtraction):
ioc_match = self.indicators.check_process(result["client"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("last_modified") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def process_db(self, file_path):
@@ -69,10 +69,7 @@ class WebkitResourceLoadStatistics(IOSExtraction):
ioc_match = self.indicators.check_url(result["registrable_domain"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("last_seen_isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def _process_observations_db(self, db_path: str, domain: str, path: str) -> None:
@@ -105,12 +105,7 @@ class WebkitSessionResourceLog(IOSExtraction):
if ioc_match:
entry, source_domains, destination_domains = record
self.alertstore.critical(
ioc_match.message,
entry.get("last_seen")
or entry.get("most_recent_interaction")
or "",
entry,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", entry, matched_indicator=ioc_match.ioc
)
redirect_path = ""
@@ -134,9 +129,7 @@ class WebkitSessionResourceLog(IOSExtraction):
self.alertstore.high(
f"Found HTTP redirect between suspicious domains: {redirect_path}",
entry.get("last_seen")
or entry.get("most_recent_interaction")
or "",
"",
entry,
)
+1 -4
View File
@@ -189,10 +189,7 @@ class Whatsapp(IOSExtraction):
):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def collect_url_results(self) -> None:
@@ -90,10 +90,4 @@ class TestDumpsysAppopsArtifact:
assert len(da.alertstore.alerts) == 3
assert len(detected_by_ioc) == 1
assert detected_by_ioc[0].matched_indicator is not None
assert detected_by_ioc[0].event_time == max(
entry["timestamp"]
for permission in detected_by_ioc[0].event["permissions"]
for entry in permission.get("entries", [])
if entry.get("timestamp")
)
assert len(detected_by_permission_heuristic) == 2
+15
View File
@@ -298,6 +298,21 @@ class TestCommand:
alerts = json.loads((tmp_path / "alerts.json").read_text())
assert alerts[0]["event"]["payload"] == "\\xa8\\xa9"
def test_records_time_and_memory_per_module(self, tmp_path):
cmd = RecordingCommand(results_path=str(tmp_path))
cmd.modules = [FirstModule, SecondModule]
cmd.run()
info = json.loads((tmp_path / "info.json").read_text())
assert [stat["module"] for stat in info["module_stats"]] == [
"FirstModule",
"SecondModule",
]
for stat in info["module_stats"]:
assert stat["seconds"] >= 0
assert stat["peak_rss_growth_mb"] >= 0
def test_stores_collected_urls(self, tmp_path):
cmd = RecordingCommand(results_path=str(tmp_path))
cmd.modules = [URLRecordingModule]
-1
View File
@@ -44,4 +44,3 @@ class TestCalendarModule:
assert len(m.results) == 1
assert len(m.timeline) == 4
assert len(m.alertstore.alerts) == 1
assert m.alertstore.alerts[0].event_time == m.results[0]["start_date"]