diff --git a/test/ci/scripts/harbomaster/__init__.py b/test/ci/scripts/harbomaster/__init__.py
index 31eec016b..1602326f6 100644
--- a/test/ci/scripts/harbomaster/__init__.py
+++ b/test/ci/scripts/harbomaster/__init__.py
@@ -1,26 +1,26 @@
 # for the module
 import sys as __hbm_sys
 
 
 def export(definition):
     """
     Decorator to export definitions from sub-modules to the top-level package
 
     :param definition: definition to be exported
     :return: definition
     """
     __module = __hbm_sys.modules[definition.__module__]
     __pkg = __hbm_sys.modules[__module.__package__]
     __pkg.__dict__[definition.__name__] = definition
 
     if '__all__' not in __pkg.__dict__:
         __pkg.__dict__['__all__'] = []
 
     __pkg.__all__.append(definition.__name__)
 
     return definition
 
 
-from . import ctestresults  # noqa
+from . import testresults   # noqa
 from . import arclint       # noqa
 from . import hbm           # noqa
diff --git a/test/ci/scripts/harbomaster/ctestresults.py b/test/ci/scripts/harbomaster/ctestresults.py
deleted file mode 100644
index ecce89a9f..000000000
--- a/test/ci/scripts/harbomaster/ctestresults.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import xml.etree.ElementTree as xml_etree
-from .results import Results
-from . import export
-
-
-@export
-class CTestResults:
-    STATUS = {'passed': Results.PASS,
-              'failed': Results.FAIL}
-
-    def __init__(self, filename):
-        self._file = open(filename, "r")
-        self._etree = xml_etree.parse(self._file)
-        self._root = self._etree.getroot()
-        self.test_format = 'CTest'
-
-    def __iter__(self):
-        self._tests = iter(self._root.findall('./Testing/Test'))
-        return self
-
-    def __next__(self):
-        class Test:
-            def __init__(self, element):
-                self.name = element.find('Name').text
-                self.path = element.find('FullName').text
-                self.status = CTestResults.STATUS[element.attrib['Status']]
-                self.duration = float(element.find(
-                    "./Results/NamedMeasurement[@name='Execution Time']/Value").text)
-                self.reason = None
-                if self.status == Results.FAIL:
-                    self.reason = element.find(
-                        "./Results/NamedMeasurement[@name='Exit Code']/Value").text
-                    if self.reason == "Timeout":
-                        self.status = Results.BROKEN
-                    else:
-                        self.reason = "{0} with exit code [{1}]\nSTDOUT:\n{2}".format(
-                            self.reason,
-                            element.find(
-                                "./Results/NamedMeasurement[@name='Exit Value']/Value").text,
-                            '\n'.join((el.text for el in element.findall("./Results/Measurement/Value"))),  # noqa: E501
-                        )
-
-        test = next(self._tests)
-
-        return Test(test)
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        self._file.close()
-
-    def __enter__(self):
-        return self
diff --git a/test/ci/scripts/harbomaster/testresults.py b/test/ci/scripts/harbomaster/testresults.py
new file mode 100644
index 000000000..805ef0e4e
--- /dev/null
+++ b/test/ci/scripts/harbomaster/testresults.py
@@ -0,0 +1,92 @@
+import xml.etree.ElementTree as xml_etree
+from collections import namedtuple
+
+from .results import Results
+from . import export
+
+
+class TestResults:
+    STATUS = {'passed': Results.PASS,
+              'failed': Results.FAIL}
+    Test = namedtuple('Test', 'name path status duration reason'.split())
+
+    def __init__(self, filename):
+        self._file = open(filename, "r")
+        self._etree = xml_etree.parse(self._file)
+        self._root = self._etree.getroot()
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self._file.close()
+
+    def __enter__(self):
+        return self
+
+
+@export
+class CTestResults(TestResults):
+    def __init__(self, filename):
+        super().__init__(self, filename)
+        self.test_format = 'CTest'
+
+    def __iter__(self):
+        self._tests = iter(self._root.findall('./Testing/Test'))
+        return self
+
+    def __next__(self):
+        element = next(self._tests)
+
+        name = element.find('Name').text
+        path = element.find('FullName').text
+        status = CTestResults.STATUS[element.attrib['Status']]
+        duration = float(element.find(
+            "./Results/NamedMeasurement[@name='Execution Time']/Value").text)
+        reason = None
+
+        if status == Results.FAIL:
+            reason = element.find(
+                "./Results/NamedMeasurement[@name='Exit Code']/Value").text
+            if reason == "Timeout":
+                status = Results.BROKEN
+            else:
+                reason = "{0} with exit code [{1}]\nSTDOUT:\n{2}".format(
+                    reason,
+                    element.find(
+                        "./Results/NamedMeasurement[@name='Exit Value']/Value").text,  # noqa: E501
+                    '\n'.join((el.text for el in element.findall("./Results/Measurement/Value"))),  # noqa: E501
+                )
+
+        return self.Test(name, path, status, duration, reason)
+
+
+@export
+class JUnitTestResults(TestResults):
+    def __init__(self, filename):
+        super().__init__(self, filename)
+        self.test_format = 'JUnit'
+
+    def __iter__(self):
+        self._tests = iter(self._root.findall('testcase'))
+        return self
+
+    def __next__(self):
+        element = next(self._tests)
+
+        name = element.attrib['name']
+        path = element.attrib['file'] \
+            + ':{}'.format(element.attrib['line'])
+        duration = element.attrib['time']
+
+        failure = element.find('failure')
+        error = element.find('error')
+
+        if failure is not None and error is not None:
+            status = Results.PASS
+
+        elif error:
+            status = Results.BROKEN
+            reason = error.attrib['message'] + '\n' + error.attrib['type']
+        elif failure:
+            status = Results.FAIL
+            reason = failure.attrib['message'] + '\n' \
+                + failure.attrib['type']
+        return self.Test(name, path, status, duration, reason)
diff --git a/test/ci/scripts/hbm b/test/ci/scripts/hbm
index 0ebd246a7..120d8dfa9 100755
--- a/test/ci/scripts/hbm
+++ b/test/ci/scripts/hbm
@@ -1,69 +1,80 @@
 #!/usr/bin/env python3
 import click
 import harbomaster
 
 @click.group()
 @click.option('-a', '--api-token', default=None, envvar='API_TOKEN')
 @click.option('-h', '--host', default=None, envvar='PHABRICATOR_HOST')
 @click.option('-b', '--build-target-phid', envvar='BUILD_TARGET_PHID')
 @click.pass_context
 def hbm(ctx, api_token, host, build_target_phid):
     ctx.obj['API_TOKEN'] = api_token
     ctx.obj['HOST'] = host
     ctx.obj['BUILD_TARGET_PHID'] = build_target_phid
     
 @hbm.command()
 @click.option('-f', '--filename')
 @click.pass_context
 def send_ctest_results(ctx, filename):
     try:
         _hbm = harbomaster.Harbormaster(ctx=ctx.obj)
         with harbomaster.CTestResults(filename) as tests:
             _hbm.send_unit_tests(tests)
     except:
         pass
 
+@hbm.command()
+@click.option('-f', '--filename')
+@click.pass_context
+def send_junit_results(ctx, filename):
+    try:
+        _hbm = harbomaster.Harbormaster(ctx=ctx.obj)
+        with harbomaster.JUnitTestResults(filename) as tests:
+            _hbm.send_unit_tests(tests)
+    except:
+        pass
+
 @hbm.command()
 @click.option('-f', '--filename')
 @click.pass_context
 def send_arc_lint(ctx, filename):
     try:
         _hbm = harbomaster.Harbormaster(ctx=ctx.obj)
         with harbomaster.ARCLintJson(filename) as tests:
             _hbm.send_lint(tests)
     except:
         pass
 
 @hbm.command()
 @click.option('-k', '--key')
 @click.option('-u', '--uri')
 @click.option('-l', '--label')
 @click.pass_context
 def send_uri(ctx, key, uri, label):
     _hbm = harbomaster.Harbormaster(ctx=ctx.obj)
     _hbm.send_uri(key, uri, label)
 
 @hbm.command()
 @click.option('-f', '--filename')
 @click.option('-n', '--name')
 @click.option('-v', '--view_policy', default=None)
 @click.pass_context
 def upload_file(ctx, filename, name, view_policy):
     _hbm = harbomaster.Harbormaster(ctx=ctx.obj)
     _hbm.upload_file(filename, name, view_policy)
     
 @hbm.command()
 @click.pass_context
 def passed(ctx):
     _hbm = harbomaster.Harbormaster(ctx=ctx.obj)
     _hbm.passed()
 
 @hbm.command()
 @click.pass_context
 def failed(ctx):
     _hbm = harbomaster.Harbormaster(ctx=ctx.obj)
     _hbm.failed()
    
 
 if __name__ == '__main__':
     hbm(obj={})