ArtifactsService: Add CPE export functionality.

Add the CPE report export to the artifacts service.
Includes the report itself and the warnings as individual artifacts.

BUG=chromium:905037
TEST=run_tests, manually ran endpoint

Change-Id: I1dc5fe22f72528f01c485037ab8f31e07b556f33
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1709145
Tested-by: Alex Klein <saklein@chromium.org>
Commit-Queue: Alex Klein <saklein@chromium.org>
Reviewed-by: Andrew Lamb <andrewlamb@chromium.org>
Reviewed-by: David Burger <dburger@chromium.org>
diff --git a/api/controller/artifacts.py b/api/controller/artifacts.py
index 7b5fc5e..09799d5 100644
--- a/api/controller/artifacts.py
+++ b/api/controller/artifacts.py
@@ -416,3 +416,35 @@
 
   for file_name in results:
     output_proto.artifacts.add().path = file_name
+
+
+@validate.exists('output_dir')
+def ExportCpeReport(input_proto, output_proto, config):
+  """Export a CPE report.
+
+  Args:
+    input_proto (BundleRequest): The input proto.
+    output_proto (BundleResponse): The output proto.
+    config (api_config.ApiConfig): The API call config.
+  """
+  chroot = controller_util.ParseChroot(input_proto.chroot)
+  output_dir = input_proto.output_dir
+
+  if input_proto.build_target.name:
+    # Legacy handling - use the default sysroot path for the build target.
+    build_target = controller_util.ParseBuildTarget(input_proto.build_target)
+    sysroot = sysroot_lib.Sysroot(build_target.root)
+  elif input_proto.sysroot.path:
+    sysroot = sysroot_lib.Sysroot(input_proto.sysroot.path)
+  else:
+    # TODO(saklein): Switch to validate decorators once legacy handling can be
+    #   cleaned up.
+    cros_build_lib.Die('sysroot.path is required.')
+
+  if config.validate_only:
+    return controller.RETURN_CODE_VALID_INPUT
+
+  cpe_result = artifacts.GenerateCpeReport(chroot, sysroot, output_dir)
+
+  output_proto.artifacts.add().path = cpe_result.report
+  output_proto.artifacts.add().path = cpe_result.warnings
diff --git a/api/controller/artifacts_unittest.py b/api/controller/artifacts_unittest.py
index 433e225..10995e2 100644
--- a/api/controller/artifacts_unittest.py
+++ b/api/controller/artifacts_unittest.py
@@ -762,3 +762,45 @@
 
     self.assertTrue(response.artifacts)
     self.assertItemsEqual(expected_files, [a.path for a in response.artifacts])
+
+
+class ExportCpeReportTest(cros_test_lib.MockTempDirTestCase,
+                          api_config.ApiConfigMixin):
+  """ExportCpeReport tests."""
+
+  def setUp(self):
+    self.response = artifacts_pb2.BundleResponse()
+
+  def testValidateOnly(self):
+    """Sanity check validate only calls don't execute."""
+    patch = self.PatchObject(artifacts_svc, 'GenerateCpeReport')
+
+    request = artifacts_pb2.BundleRequest()
+    request.build_target.name = 'board'
+    request.output_dir = self.tempdir
+
+    artifacts.ExportCpeReport(request, self.response, self.validate_only_config)
+
+    patch.assert_not_called()
+
+  def testNoBuildTarget(self):
+    request = artifacts_pb2.BundleRequest()
+    request.output_dir = self.tempdir
+
+    with self.assertRaises(cros_build_lib.DieSystemExit):
+      artifacts.ExportCpeReport(request, self.response, self.api_config)
+
+  def testSuccess(self):
+    """Test success case."""
+    expected = artifacts_svc.CpeResult(
+        report='/output/report.json', warnings='/output/warnings.json')
+    self.PatchObject(artifacts_svc, 'GenerateCpeReport', return_value=expected)
+
+    request = artifacts_pb2.BundleRequest()
+    request.build_target.name = 'board'
+    request.output_dir = self.tempdir
+
+    artifacts.ExportCpeReport(request, self.response, self.api_config)
+
+    for artifact in self.response.artifacts:
+      self.assertIn(artifact.path, [expected.report, expected.warnings])