first commit

This commit is contained in:
Jean-Marie Mineau 2023-11-15 15:59:13 +01:00
commit cd1e91bb99
Signed by: histausse
GPG key ID: B66AEEDA9B645AD2
287 changed files with 86425 additions and 0 deletions

View file

@ -0,0 +1,46 @@
from importlib import import_module
import pkgutil
from pathlib import Path
import os
MODULE_PATH = os.path.dirname(os.path.realpath(__file__))
#print(MODULE_PATH)
__cls_cache = dict()
def factory(name: str, **kwargs):
name = name + '_tester'
mod = import_module('.' + name, package='tester')
tester_cls = getattr(mod, name)
instance = tester_cls(**kwargs)
return instance
def factory_cls(name: str, **kwargs):
name = str(name) + '_tester'
if name in __cls_cache:
return __cls_cache[name]
else:
mod = import_module('.' + name, package='tester')
tester_cls = getattr(mod, name)
__cls_cache[name] = tester_cls
return tester_cls
def analyse_artifacts(tool_name: str, path: Path, apk_filename: str):
tester_cls = factory_cls(tool_name)
return tester_cls.analyse_artifacts(path, apk_filename)
def check_success(tool_name: str, path: Path, apk_filename: str):
tester_cls = factory_cls(tool_name)
return tester_cls.check_success(path, apk_filename)
def get_all_testers():
tmp = []
for importer, modname, ispkg in pkgutil.iter_modules([MODULE_PATH]):
if (ispkg is False) and ('abstract' not in modname) and ('_tester' in modname):
tmp.append(modname)
return tmp

View file

@ -0,0 +1,51 @@
from abc import ABC, abstractmethod
from pathlib import Path
import utils
import datetime
import error_collector as errors
class abstract_tester(ABC):
"""
Base class for too tester.
Sub-classes MUST define TOOL_NAME and EXPECTED_ERROR_TYPES
"""
def __init__(self):
super().__init__()
@classmethod
def analyse_artifacts(cls, path: Path, apk_filename: str):
"""Analyse the artifacts of a test located at `path`."""
try:
report = utils.parse_report(path / "report")
report["errors"] = [
e.get_dict()
for e in errors.get_errors(path / "stderr", cls.EXPECTED_ERROR_TYPES)
]
report["errors"].extend(
[
e.get_dict()
for e in errors.get_errors(path / "stdout", cls.EXPECTED_ERROR_TYPES)
]
)
if report["timeout"]:
report["tool-status"] = "TIMEOUT"
elif cls.check_success(path, apk_filename):
report["tool-status"] = "FINISHED"
else:
report["tool-status"] = "FAILED"
except Exception as e:
report = {}
report["tool-status"] = "UNKNOWN"
report["analyser-error"] = str(e)
report["tool-name"] = cls.TOOL_NAME
report["date"] = str(datetime.datetime.now())
report["apk"] = apk_filename
return report
@classmethod
@abstractmethod
def check_success(cls, path: Path, apk_filename: str):
pass

View file

@ -0,0 +1,29 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
from typing import Type
from pathlib import Path
class adagio_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [errors.PythonError]
TOOL_NAME = 'adagio'
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
"""Check if the analysis finished without crashing."""
apks = list(path.glob("*.apk"))
if len(apks) != 1:
raise RuntimeError(
# FIXME: do not raise in check_success. Return False instead
f"Expected to found exactly 1 apk in the root of artifact folder, found {apks}"
)
apk = apks[0]
path_result = path / utils.sha256_sum(apk).lower()
return path_result.exists()

View file

@ -0,0 +1,27 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
from typing import Type
from pathlib import Path
class amandroid_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
]
TOOL_NAME = "amandroid"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
return (
path
/ "out"
/ utils.removesuffix(apk_filename, ".apk")
/ "result"
/ "AppData.txt"
).exists()

View file

@ -0,0 +1,34 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
from typing import Type
from pathlib import Path
class anadroid_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.PythonError,
]
TOOL_NAME = "anadroid"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
stdout = path / "stdout"
# Apktool failled
if not (
path / utils.removesuffix(apk_filename, ".apk") / "apktool.yml"
).exists():
return False
with stdout.open("r", errors="replace") as f:
for line in f:
if (
"ee3d6c7015b83b3dc84b21a2e79506175f07c00ecf03e7b3b8edea4e445618bd: END OF ANALYSIS."
in line
):
return True
return False

View file

@ -0,0 +1,26 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
from typing import Type
from pathlib import Path
class androguard_dad_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [errors.Python311Error]
TOOL_NAME = "androguard_dad"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
stdout = path / "stdout"
with stdout.open("r", errors="replace") as f:
for line in f:
if (
"ee3d6c7015b83b3dc84b21a2e79506175f07c00ecf03e7b3b8edea4e445618bd: END OF ANALYSIS."
in line
):
return True
return False

View file

@ -0,0 +1,26 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
from typing import Type
from pathlib import Path
class androguard_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [errors.Python311Error]
TOOL_NAME = "androguard"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
stdout = path / "stdout"
with stdout.open("r", errors="replace") as f:
for line in f:
if (
"ee3d6c7015b83b3dc84b21a2e79506175f07c00ecf03e7b3b8edea4e445618bd: END OF ANALYSIS."
in line
):
return True
return False

View file

@ -0,0 +1,30 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
class apparecium_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [errors.PythonError]
TOOL_NAME = "apparecium"
SOURCE_SINK_RE = re.compile(r"(\d+) sources, (\d+) sinks")
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
with (path / "stdout").open() as f:
for line in f:
m = apparecium_tester.SOURCE_SINK_RE.match(line)
if m is not None and (int(m.group(1)) == 0 or int(m.group(2)) == 0):
return True
if line.strip() in [
"potential data leakage: YES",
"potential data leakage: NO",
]:
return True
return False

View file

@ -0,0 +1,29 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
from typing import Type
from pathlib import Path
class blueseal_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [errors.JavaError, errors.NoPrefixJavaError]
TOOL_NAME = 'blueseal'
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
l1 = False
with (path / "stdout").open("r", errors="replace") as stdout:
for line in stdout:
if l1 and "Soot has run for " in line:
return True
l1 = False
if "Soot finished on " in line:
l1 = True
return False

View file

@ -0,0 +1,40 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
class dialdroid_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
errors.FlowdroidLog4jError,
]
TOOL_NAME = "dialdroid"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
l1 = False
l2 = False
with (path / "stdout").open(errors="replace") as file:
for line in file:
if (
l2
and line.strip()
== f"Done:{utils.removesuffix(apk_filename.lower(), '.apk')}"
):
return True
l2 = False
if l1 and "Analysis has run for" in line:
l2 = True
l1 = False
if "Maximum memory consumption:" in line:
l1 = True
return False

View file

@ -0,0 +1,78 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
from typing import Type
from pathlib import Path
class didfail_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [errors.PythonError]
TOOL_NAME = 'didfail'
EXPECTED_ERROR_TYPES_FLOWDROID: list = [
errors.JavaError,
errors.NoPrefixJavaError,
errors.FlowdroidLog4jError,
]
EXPECTED_ERROR_TYPES_XFORM: list = [
errors.JavaError,
errors.NoPrefixJavaError,
errors.FlowdroidLog4jError,
]
EXPECTED_ERROR_TYPES_DARE: list = []
def __init__(self):
super().__init__()
@classmethod
def analyse_artifacts(cls, path: Path, apk_filename: str):
"""Analyse the artifacts of a test located at `path`."""
apk_basename = apk_filename.rstrip('.apk')
report = utils.parse_report(path / "report")
report["errors"] = []
flowdroid_log = path / "out" / "log" / (apk_basename+".flowdroid.log")
dare_log = path / "out" / "log" / (apk_basename+".dare.log")
xform_log = path / "out" / "log" / (apk_basename+".xform.log")
report["errors"].extend(
[e.get_dict() for e in errors.get_errors(path / "stdout", cls.EXPECTED_ERROR_TYPES)]
)
if flowdroid_log.exists():
report["errors"].extend(
[e.get_dict() for e in errors.get_errors(flowdroid_log, cls.EXPECTED_ERROR_TYPES_FLOWDROID)]
)
if dare_log.exists():
report["errors"].extend(
[e.get_dict() for e in errors.get_errors(dare_log, cls.EXPECTED_ERROR_TYPES_DARE)]
)
if xform_log.exists():
report["errors"].extend(
[e.get_dict() for e in errors.get_errors(xform_log, cls.EXPECTED_ERROR_TYPES_XFORM)]
)
if report["timeout"]:
report["tool-status"] = "TIMEOUT"
elif cls.check_success(path, apk_filename) and (report["exit-status"] == 0):
report["tool-status"] = "FINISHED"
else:
report["tool-status"] = "FAILED"
report["tool-name"] = cls.TOOL_NAME
report["date"] = str(datetime.datetime.now())
report["apk"] = apk_filename
return report
@classmethod
def check_success(cls, path: Path, apk_filename: str):
"""Check if the analysis finished without crashing."""
with (path / "stdout").open("r", errors="replace") as file:
for line in file:
if line == "Failure!\n":
return False
flowfile = path / "out" / "flows.out"
if not flowfile.exists():
return False
return flowfile.stat().st_size > 1

View file

@ -0,0 +1,24 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
from typing import Type
from pathlib import Path
class droidsafe_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
errors.DroidsafeLog4jError,
]
TOOL_NAME = "droidsafe"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
return (path / "droidsafe-gen" / "info-flow-results.txt").exists() and (
path / "droidsafe-gen" / "template-spec.ssl"
).exists()

View file

@ -0,0 +1,36 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
class flowdroid_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
errors.FlowdroidLog4jError,
]
TOOL_NAME = "flowdroid"
RE_SUCCESS = re.compile(
r"\[.*?\] INFO soot.jimple.infoflow.android.SetupApplication\$InPlaceInfoflow - Data flow solver took (\d*) seconds. Maximum memory consumption: (\d*) MB\n"
r"\[.*?\] INFO soot.jimple.infoflow.android.SetupApplication - Found (\d*) leaks",
re.MULTILINE,
)
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
report = utils.parse_report(path / "report")
l1, l2 = "", ""
# TODO: find a better way to do it
with (path / "stderr").open("r", errors="replace") as file:
for l in file:
l1, l2 = l2, l
last_lines = l1 + l2
match = flowdroid_tester.RE_SUCCESS.match(last_lines)
return match is not None

View file

@ -0,0 +1,30 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
class gator_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
errors.FlowdroidLog4jError,
errors.PythonError,
]
TOOL_NAME = "gator"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
if len(list(path.glob("null-DEBUG-*.txt"))) == 0:
return False
with (path / "stdout").open("r", errors="replace") as file:
for line in file:
if "</GUIHierarchy>" in line:
return True
return False

View file

@ -0,0 +1,23 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
class ic3_fork_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
]
TOOL_NAME = "ic3_fork"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
"""Check if the analysis finished without crashing."""
return len(list((path / "ic3_out").iterdir())) >= 1

View file

@ -0,0 +1,33 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
class ic3_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
]
TOOL_NAME = "ic3"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
"""Check if the analysis finished without crashing."""
if (path / "dare_out").exists():
# if the tool use dare, check that dare succed
if not (
path
/ "dare_out"
/ "retargeted"
/ utils.removesuffix(apk_filename, ".apk")
/ "classes.txt"
).exists():
return False
return len(list((path / "ic3_out").iterdir())) >= 1

View file

@ -0,0 +1,31 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
class iccta_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
errors.FlowdroidLog4jError,
]
TOOL_NAME = "iccta"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
l1 = False
with (path / "stdout").open(errors="replace") as file:
for line in file:
if l1 and "Analysis has run for" in line:
return True
l1 = False
if "Maximum memory consumption:" in line:
l1 = True
return False

View file

@ -0,0 +1,27 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
class mallodroid_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.PythonError
]
TOOL_NAME = "mallodroid"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
"""Check if the analysis finished without crashing."""
stdout = path / "stdout"
with stdout.open("r", errors="replace") as f:
for line in f:
if "ee3d6c7015b83b3dc84b21a2e79506175f07c00ecf03e7b3b8edea4e445618bd: END OF ANALYSIS." in line:
return True
return False

View file

@ -0,0 +1,32 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
class perfchecker_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
errors.FlowdroidLog4jError,
]
TOOL_NAME = "perfchecker"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
START = "***********************analysis results**********************"
END = "********************end of analysis results******************"
started = False
with (path / "stdout").open("r", errors="replace") as file:
for line in file:
if line.strip() == START:
started = True
if started and line.strip() == END:
return True
return False

View file

@ -0,0 +1,118 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
from typing import Type, Optional, Any
from pathlib import Path
import re
from more_itertools import peekable
# TODO: Why isn't that in error_collector with the other error types?
class OcamlError(errors.LoggedError):
error_re = re.compile(r"(Exception|Fatal error): (.*)")
raised_at_re = re.compile(
r"Raised at (.*?) in file \"(.*?)\", line (\d*?), characters .*"
)
called_from_re = re.compile(
r"Called from (.*?) in file \"(.*?)\", line (\d*?), characters .*"
)
def __init__(
self,
first_line_nb: int,
last_line_nb: int,
level: str,
msg: str,
raised_info: Optional[dict],
called_info: Optional[dict],
logfile_name: str = "",
):
self.first_line_nb = first_line_nb
self.last_line_nb = last_line_nb
self.level = level
self.msg = msg
self.raised_info = raised_info
self.called_info = called_info
self.logfile_name = logfile_name
def __str__(self):
l1 = f"{self.level}: {self.msg}"
if self.raised_info is not None:
l2 = f"\nRaised at {self.raised_info['function']} in file \"{self.raised_info['file']}\", line {self.raised_info['line']}"
else:
l2 = ""
if self.called_info is not None:
l2 = f"\nCalled from {self.called_info['function']} in file \"{self.called_info['file']}\", line {self.called_info['line']}"
else:
l2 = ""
return f"{self.level}: {self.msg}"
def get_dict(self) -> dict:
return {
"error_type": "Ocaml",
"level": self.level,
"msg": self.msg,
"first_line": self.first_line_nb,
"last_line": self.last_line_nb,
"raised_info": self.raised_info,
"called_info": self.called_info,
"logfile_name": self.logfile_name,
}
@staticmethod
def parse_error(logs: peekable) -> Optional["OcamlError"]:
line_nb, line = logs.peek((None, None))
if line is None or line_nb is None:
return None
match = OcamlError.error_re.match(line)
if match is None:
return None
error = OcamlError(line_nb, line_nb, match.group(1), match.group(2), None, None)
next(logs)
line_nb, line = logs.peek((None, None))
if line is None or line_nb is None:
return error
match = OcamlError.raised_at_re.match(line)
if match is None:
return error
error.raised_info = {
"function": match.group(1),
"file": match.group(2),
"line": match.group(3),
}
next(logs)
line_nb, line = logs.peek((None, None))
if line is None or line_nb is None:
return error
match = OcamlError.called_from_re.match(line)
if match is None:
return error
error.called_info = {
"function": match.group(1),
"file": match.group(2),
"line": match.group(3),
}
return error
class redexer_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [OcamlError, errors.RubyError]
TOOL_NAME = 'redexer'
EXPECTED_ERROR_TYPES_STDOUT: list = [
errors.JavaError, # For apktool
] # TODO: add log4j format for apktool
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
new_apk = path / "new.apk"
return new_apk.exists()

View file

@ -0,0 +1,106 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
from typing import Any, Type, Optional
from more_itertools import peekable
import xml.etree.ElementTree as xmltree
class SaafLog4jError(errors.LoggedError):
error_re = re.compile(
r"\d\d .*? \d\d\d\d \d\d:\d\d:\d\d,\d*? \[.*?\] (ERROR|FATAL) (.*?) - (.*)$"
)
def __init__(
self,
first_line_nb: int,
last_line_nb: int,
level: str,
origin: str,
msg: str,
logfile_name: str = "",
):
self.first_line_nb = first_line_nb
self.last_line_nb = last_line_nb
self.level = level
self.origin = origin
self.msg = msg
self.logfile_name = logfile_name
def __str__(self) -> str:
return f"{self.level} {self.origin} {self.msg}"
def get_dict(self) -> dict:
return {
"error_type": "Log4j",
"level": self.level,
"origin": self.origin,
"msg": self.msg,
"first_line": self.first_line_nb,
"last_line": self.last_line_nb,
"logfile_name": self.logfile_name,
}
@staticmethod
def parse_error(logs: peekable) -> Optional["SaafLog4jError"]:
line_nb, line = logs.peek((None, None))
if line is None or line_nb is None:
return None
match = SaafLog4jError.error_re.match(line)
if match is None:
return None
error = SaafLog4jError(
line_nb, line_nb, match.group(1), match.group(2), match.group(3)
)
next(logs)
return error
class saaf_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
SaafLog4jError,
]
TOOL_NAME = "saaf"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
"""Check if the analysis finished without crashing."""
# uncritical = None
critical = None
with (path / "stdout").open("r", errors="replace") as stdout:
for line in stdout:
# if "#Analyses w/ uncritical exceptions:" in line:
# uncritical = int(
# utils.removeprefix(line, "#Analyses w/ uncritical exceptions:").strip()
# )
if "#Critical Exceptions:" in line:
critical = int(
utils.removeprefix(line, "#Critical Exceptions:").strip()
)
# if uncritical is None or critical is None:
# return False
if critical is None:
return False
if critical != 0:
return False
rprts = list((path / "rprt").glob("*.xml"))
if len(rprts) != 1:
return False
rprt = rprts[0]
tree = xmltree.parse(rprt)
msgs = tree.findall("./status/message")
if len(msgs) != 1:
return False
msg = msgs[0]
if msg.text != "FINISHED":
return False
return True

View file

@ -0,0 +1,66 @@
from .abstract_tester import abstract_tester
import utils
import error_collector as errors
import datetime
import re
from typing import Type
from pathlib import Path
from typing import Optional
from more_itertools import peekable
class XsbError(errors.LoggedError):
def __init__(
self,
first_line_nb: int,
last_line_nb: int,
msg: str,
logfile_name: str = "",
):
self.first_line_nb = first_line_nb
self.last_line_nb = last_line_nb
self.msg = msg
self.logfile_name = logfile_name
def __str__(self):
return f"++Error{self.msg}"
def get_dict(self) -> dict:
return {
"error_type": "Xsb",
"msg": self.msg,
"first_line": self.first_line_nb,
"last_line": self.last_line_nb,
"logfile_name": self.logfile_name,
}
@staticmethod
def parse_error(logs: peekable) -> Optional["XsbError"]:
line_nb, line = logs.peek((None, None))
if line is None or line_nb is None:
return None
if not line.startswith("++Error"):
return None
error = XsbError(line_nb, line_nb, line.strip())
next(logs)
return error
class wognsen_et_al_tester(abstract_tester):
EXPECTED_ERROR_TYPES: list = [
errors.JavaError,
errors.NoPrefixJavaError,
errors.PythonError,
XsbError,
]
TOOL_NAME = "wognsen_et_al"
def __init__(self):
super().__init__()
@classmethod
def check_success(cls, path: Path, apk_filename: str):
"""Check if the analysis finished without crashing."""
# The tool is supposed to print the graph to stdout, if stdout
# is empty, it means the tool failed.
return (path / "stdout").stat().st_size > 1