Skip to content

Commit

Permalink
Add support for FuzzManager test cases that are not zip files
Browse files Browse the repository at this point in the history
  • Loading branch information
tysmith committed Jan 8, 2024
1 parent 54b04f4 commit b0715b9
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 31 deletions.
61 changes: 43 additions & 18 deletions grizzly/common/fuzzmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from contextlib import contextmanager
from logging import getLogger
from pathlib import Path
from shutil import rmtree
from re import search
from shutil import copyfileobj, rmtree
from tempfile import NamedTemporaryFile, mkdtemp
from zipfile import ZipFile
from zipfile import BadZipFile, ZipFile

from Collector.Collector import Collector
from FTB.ProgramConfiguration import ProgramConfiguration
Expand Down Expand Up @@ -295,7 +296,7 @@ def _subset(tests, subset):
# build list of items to preserve
return [tests[i] for i in sorted(keep)]

def testcases(self, subset=None):
def testcases(self, subset=None, ext=None):
"""Download the testcase data from CrashManager.
Arguments:
Expand All @@ -317,27 +318,51 @@ def testcases(self, subset=None):
dir=grz_tmp("fuzzmanager"),
prefix=f"crash-{self.crash_id}-",
suffix=Path(self.testcase).suffix,
) as archive:
archive.write(response.content)
archive.seek(0)
) as data:
data.write(response.content)
data.seek(0)
# self._storage should be removed when self.cleanup() is called
self._storage = Path(
mkdtemp(
prefix=f"crash-{self.crash_id}-", dir=grz_tmp("fuzzmanager")
)
)
with ZipFile(archive) as zip_fp:
zip_fp.extractall(path=self._storage)
# test case directories are named sequentially, a zip with three test
# directories would have:
# - 'foo-2' (oldest)
# - 'foo-1'
# - 'foo-0' (most recent)
# see FuzzManagerReporter for more info
self._contents = sorted(
(x.parent for x in self._storage.rglob(TEST_INFO)),
reverse=True,
)
try:
with ZipFile(data) as zip_fp:
zip_fp.extractall(path=self._storage)
# test case directories are named sequentially
# an archive with three test directories would have:
# - 'foo-2' (oldest)
# - 'foo-1'
# - 'foo-0' (most recent)
# see FuzzManagerReporter for more info
self._contents = sorted(
(x.parent for x in self._storage.rglob(TEST_INFO)),
reverse=True,
)
except BadZipFile as exc:
if ext is None:
match = search(
r'filename="(?P<name>.+)"',
response.headers["content-disposition"],
)
if match:
# if nothing is found fallback to html
file_name = match.group("name")
if "." in file_name:
ext = file_name.split(".")[-1] or "html"
else:
ext = "html"
if ext.lower().endswith("zip"):
LOG.error("Error loading test case: %s", exc)
self._contents = []
else:
# load raw test case
test_file = self._storage / f"test.{ext}"
data.seek(0)
with test_file.open("wb") as dst:
copyfileobj(data, dst)
self._contents = [test_file]

if subset and self._contents:
return self._subset(self._contents, subset)
Expand Down
69 changes: 57 additions & 12 deletions grizzly/common/test_fuzzmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .storage import TEST_INFO


def test_bucket_1(mocker):
def test_bucket_01(mocker):
"""bucket getattr uses data from get"""
coll = mocker.patch("grizzly.common.fuzzmanager.Collector", autospec=True)
coll.return_value.get.return_value.json.return_value = {"testcase": "data"}
Expand All @@ -28,7 +28,7 @@ def test_bucket_1(mocker):
assert coll.return_value.get.call_count == 1


def test_bucket_2(mocker):
def test_bucket_02(mocker):
"""bucket setattr raises"""
coll = mocker.patch("grizzly.common.fuzzmanager.Collector", autospec=True)
coll.return_value.serverProtocol = "http"
Expand All @@ -40,7 +40,7 @@ def test_bucket_2(mocker):
assert coll.return_value.get.call_count == 0


def test_bucket_3(mocker):
def test_bucket_03(mocker):
"""bucket iter_crashes flattens across pages"""
coll = mocker.patch("grizzly.common.fuzzmanager.Collector", autospec=True)
coll.return_value.serverProtocol = "http"
Expand Down Expand Up @@ -80,7 +80,7 @@ def test_bucket_3(mocker):
assert crashes[1].crash_id == 456


def test_bucket_4(mocker):
def test_bucket_04(mocker):
"""bucket signature_path writes and returns sig json and metadata"""
coll = mocker.patch("grizzly.common.fuzzmanager.Collector", autospec=True)
coll.return_value.serverProtocol = "http"
Expand Down Expand Up @@ -111,7 +111,7 @@ def test_bucket_4(mocker):
assert coll.return_value.get.call_count == 1


def test_crash_1(mocker):
def test_crash_01(mocker):
"""crash getattr uses data from get"""
coll = mocker.patch("grizzly.common.fuzzmanager.Collector", autospec=True)
coll.return_value.get.return_value.json.return_value = {"testcase": "data"}
Expand All @@ -133,7 +133,7 @@ def test_crash_1(mocker):
assert coll.return_value.get.call_args[1]["params"] == {"include_raw": "1"}


def test_crash_2(mocker):
def test_crash_02(mocker):
"""crash setattr raises except testcase_quality"""
coll = mocker.patch("grizzly.common.fuzzmanager.Collector", autospec=True)
coll.return_value.get.return_value.json.return_value = {"testcase": "data"}
Expand Down Expand Up @@ -163,7 +163,52 @@ def test_crash_2(mocker):
assert coll.return_value.get.call_count == 1


def test_crash_3(mocker, tmp_path):
@mark.parametrize(
"file_name, passed_ext, expected",
[
# using existing name
("foo.html", None, "test.html"),
# use default extension
("foo", None, "test.html"),
# use default extension
("foo.", None, "test.html"),
# add missing extension
("foo", "svg", "test.svg"),
# overwrite extension
("foo.zip", "svg", "test.svg"),
# bad zipfile
("foo.zip", None, None),
],
)
def test_crash_03(mocker, tmp_path, file_name, passed_ext, expected):
"""test case is not zipfile"""
coll = mocker.patch("grizzly.common.fuzzmanager.Collector", autospec=True)
coll.return_value.serverProtocol = "http"
coll.return_value.serverPort = 123
coll.return_value.serverHost = "allizom.org"
coll.return_value.get.return_value.json.return_value = {
"id": 234,
"testcase": file_name,
}
with open(tmp_path / file_name, "w") as zip_fp:
zip_fp.write("data")
with CrashEntry(234) as crash:
assert crash.testcase == file_name # pre-load data dict so I can re-patch get
coll.return_value.get.return_value = mocker.Mock(
content=(tmp_path / file_name).read_bytes(),
headers={"content-disposition": f'attachment; filename="bar/{file_name}"'},
)
assert coll.return_value.get.call_count == 1
tests = crash.testcases(ext=passed_ext)
assert crash._contents is not None
if expected is not None:
assert tests
assert tests[-1].name == expected
else:
assert not tests


def test_crash_04(mocker, tmp_path):
"""crash testcase_path writes and returns testcase zip"""
coll = mocker.patch("grizzly.common.fuzzmanager.Collector", autospec=True)
coll.return_value.serverProtocol = "http"
Expand All @@ -185,7 +230,7 @@ def test_crash_3(mocker, tmp_path):
assert crash.testcase == "test.zip" # pre-load data dict so I can re-patch get
coll.return_value.get.return_value = mocker.Mock(
content=(tmp_path / "test.zip").read_bytes(),
headers={"content-disposition"},
headers={"content-disposition": 'attachment; filename="tests/test.zip"'},
)
assert coll.return_value.get.call_count == 1
tests = crash.testcases()
Expand All @@ -209,7 +254,7 @@ def test_crash_3(mocker, tmp_path):
assert coll.return_value.get.call_count == 2


def test_crash_4(mocker):
def test_crash_05(mocker):
"""crash create_signature writes and returns signature path"""
cfg = ProgramConfiguration("product", "platform", "os")
mocker.patch(
Expand Down Expand Up @@ -243,7 +288,7 @@ def test_crash_4(mocker):
assert coll.return_value.get.call_count == 1


def test_crash_5(mocker):
def test_crash_06(mocker):
"""crash create_signature raises when it can't create a signature"""
cfg = ProgramConfiguration("product", "platform", "os")
mocker.patch(
Expand All @@ -265,7 +310,7 @@ def test_crash_5(mocker):
assert coll.return_value.get.call_count == 1


def test_crash_6(tmp_path):
def test_crash_07(tmp_path):
"""test CrashEntry._subset()"""
# test single entry
paths = [tmp_path / "0"]
Expand Down Expand Up @@ -303,7 +348,7 @@ def test_crash_6(tmp_path):
(111, True),
],
)
def test_load_fm_data_1(mocker, bucket_id, load_bucket):
def test_load_fm_data_01(mocker, bucket_id, load_bucket):
"""test load_fm_data()"""
coll = mocker.patch("grizzly.common.fuzzmanager.Collector", autospec=True)
coll.return_value.serverProtocol = "http"
Expand Down
2 changes: 2 additions & 0 deletions grizzly/replay/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ def __init__(self):
super().__init__()
self.parser.add_argument("input", type=int, help="FuzzManager ID to replay")

self.parser.add_argument("--test-ext", help="Overwrite testcase file extension")

self.parser.add_argument(
"--test-index",
default=[],
Expand Down
2 changes: 1 addition & 1 deletion grizzly/replay/crash.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def modify_args(args, crash, bucket):
# use the newest test case when not using a harness and test_index is not specified
if args.no_harness and not args.test_index:
args.test_index = [-1]
args.input = crash.testcases(subset=args.test_index)
args.input = crash.testcases(subset=args.test_index, ext=args.test_ext)
# set tool name using crash entry
if args.tool is None:
LOG.info("Setting default --tool=%s from CrashEntry", crash.tool)
Expand Down

0 comments on commit b0715b9

Please sign in to comment.