Submit a sample of notification images to Safe Browsing

These will be scanned for social engineering behavior.

Only uploads if all of the following are true:
- User has opted in to SBER_LEVEL_SCOUT.
- Origin is not on CSD phishing whitelist.
- Device has sent < 5 reports in last 24 hours (see persistence TODO).
- Device is part of the enabled experiment group.

The notification image bitmap is downscaled to <= 512x512, encoded as a
PNG, then sent to the CSD server as a NotificationImageReportRequest
protobuf from chrome/common/safe_browsing/csd.proto.

BUG=678443

Review-Url: https://codereview.chromium.org/2637153002
Cr-Commit-Position: refs/heads/master@{#445365}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 8991997..094508f 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1993,6 +1993,8 @@
       "safe_browsing/certificate_reporting_service.h",
       "safe_browsing/certificate_reporting_service_factory.cc",
       "safe_browsing/certificate_reporting_service_factory.h",
+      "safe_browsing/notification_image_reporter.cc",
+      "safe_browsing/notification_image_reporter.h",
       "safe_browsing/permission_reporter.cc",
       "safe_browsing/permission_reporter.h",
       "safe_browsing/ping_manager.cc",
diff --git a/chrome/browser/notifications/platform_notification_service_impl.cc b/chrome/browser/notifications/platform_notification_service_impl.cc
index 25bcadd..a03b72c 100644
--- a/chrome/browser/notifications/platform_notification_service_impl.cc
+++ b/chrome/browser/notifications/platform_notification_service_impl.cc
@@ -21,6 +21,8 @@
 #include "chrome/browser/profiles/profile_attributes_storage.h"
 #include "chrome/browser/profiles/profile_io_data.h"
 #include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/safe_browsing/ping_manager.h"
+#include "chrome/browser/safe_browsing/safe_browsing_service.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/grit/generated_resources.h"
@@ -36,6 +38,7 @@
 #include "content/public/common/notification_resources.h"
 #include "content/public/common/platform_notification_data.h"
 #include "extensions/features/features.h"
+#include "third_party/skia/include/core/SkBitmap.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/base/resource/resource_bundle.h"
 #include "ui/message_center/notification.h"
@@ -87,6 +90,18 @@
                  notification_id));
 }
 
+void ReportNotificationImageOnIOThread(
+    scoped_refptr<safe_browsing::SafeBrowsingService> safe_browsing_service,
+    Profile* profile,
+    const GURL& origin,
+    const SkBitmap& image) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  if (!safe_browsing_service || !safe_browsing_service->enabled())
+    return;
+  safe_browsing_service->ping_manager()->ReportNotificationImage(
+      profile, safe_browsing_service->database_manager(), origin, image);
+}
+
 }  // namespace
 
 // static
@@ -428,6 +443,13 @@
     notification.set_type(message_center::NOTIFICATION_TYPE_IMAGE);
     notification.set_image(
         gfx::Image::CreateFrom1xBitmap(notification_resources.image));
+    // n.b. this should only be posted once per notification.
+    BrowserThread::PostTask(
+        BrowserThread::IO, FROM_HERE,
+        base::Bind(
+            &ReportNotificationImageOnIOThread,
+            make_scoped_refptr(g_browser_process->safe_browsing_service()),
+            profile, origin, notification_resources.image));
   }
 
   // Badges are only supported on Android, primarily because it's the only
diff --git a/chrome/browser/notifications/platform_notification_service_impl.h b/chrome/browser/notifications/platform_notification_service_impl.h
index feb64404..12c6e618 100644
--- a/chrome/browser/notifications/platform_notification_service_impl.h
+++ b/chrome/browser/notifications/platform_notification_service_impl.h
@@ -119,7 +119,8 @@
   void OnCloseEventDispatchComplete(
       content::PersistentNotificationStatus status);
 
-  // Creates a new Web Notification-based Notification object.
+  // Creates a new Web Notification-based Notification object. Should only be
+  // called when the notification is first shown.
   // TODO(peter): |delegate| can be a scoped_refptr, but properly passing this
   // through requires changing a whole lot of Notification constructor calls.
   Notification CreateNotificationFromData(
diff --git a/chrome/browser/safe_browsing/notification_image_reporter.cc b/chrome/browser/safe_browsing/notification_image_reporter.cc
new file mode 100644
index 0000000..984fc43
--- /dev/null
+++ b/chrome/browser/safe_browsing/notification_image_reporter.cc
@@ -0,0 +1,225 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/safe_browsing/notification_image_reporter.h"
+
+#include <cmath>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/feature_list.h"
+#include "base/logging.h"
+#include "base/memory/ptr_util.h"
+#include "base/memory/ref_counted_memory.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/rand_util.h"
+#include "base/threading/sequenced_worker_pool.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/safe_browsing/safe_browsing_service.h"
+#include "chrome/common/safe_browsing/csd.pb.h"
+#include "components/safe_browsing_db/database_manager.h"
+#include "components/safe_browsing_db/safe_browsing_prefs.h"
+#include "components/variations/variations_associated_data.h"
+#include "content/public/browser/browser_thread.h"
+#include "net/base/net_errors.h"
+#include "net/url_request/report_sender.h"
+#include "skia/ext/image_operations.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/gfx/codec/png_codec.h"
+#include "ui/gfx/geometry/size.h"
+#include "url/gurl.h"
+
+using content::BrowserThread;
+
+namespace safe_browsing {
+
+namespace {
+
+const size_t kMaxReportsPerDay = 5;
+const base::Feature kNotificationImageReporterFeature{
+    "NotificationImageReporterFeature", base::FEATURE_ENABLED_BY_DEFAULT};
+const char kReportChance[] = "ReportChance";
+const char kDefaultMimeType[] = "image/png";
+
+// Passed to ReportSender::Send as an ErrorCallback, so must take a GURL, but it
+// is unused.
+void LogReportResult(const GURL& url, int net_error) {
+  UMA_HISTOGRAM_SPARSE_SLOWLY("SafeBrowsing.NotificationImageReporter.NetError",
+                              net_error);
+}
+
+}  // namespace
+
+const char NotificationImageReporter::kReportingUploadUrl[] =
+    "https://safebrowsing.googleusercontent.com/safebrowsing/clientreport/"
+    "notification-image";
+
+NotificationImageReporter::NotificationImageReporter(
+    net::URLRequestContext* request_context)
+    : NotificationImageReporter(base::MakeUnique<net::ReportSender>(
+          request_context,
+          net::ReportSender::CookiesPreference::DO_NOT_SEND_COOKIES)) {}
+
+NotificationImageReporter::NotificationImageReporter(
+    std::unique_ptr<net::ReportSender> report_sender)
+    : report_sender_(std::move(report_sender)), weak_factory_on_io_(this) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+}
+
+NotificationImageReporter::~NotificationImageReporter() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+}
+
+void NotificationImageReporter::ReportNotificationImageOnIO(
+    Profile* profile,
+    const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
+    const GURL& origin,
+    const SkBitmap& image) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DCHECK(profile);
+  DCHECK_EQ(origin, origin.GetOrigin());
+  DCHECK(origin.is_valid());
+
+  // Skip whitelisted origins to cut down on report volume.
+  if (!database_manager || database_manager->MatchCsdWhitelistUrl(origin)) {
+    SkippedReporting();
+    return;
+  }
+
+  // Sample a Finch-controlled fraction only.
+  double report_chance = GetReportChance();
+  if (base::RandDouble() >= report_chance) {
+    SkippedReporting();
+    return;
+  }
+
+  // Avoid exceeding kMaxReportsPerDay.
+  base::Time a_day_ago = base::Time::Now() - base::TimeDelta::FromDays(1);
+  while (!report_times_.empty() &&
+         report_times_.front() < /* older than */ a_day_ago) {
+    report_times_.pop();
+  }
+  if (report_times_.size() >= kMaxReportsPerDay) {
+    SkippedReporting();
+    return;
+  }
+  // n.b. we write to report_times_ here even if we'll later end up skipping
+  // reporting because GetExtendedReportingLevel was not SBER_LEVEL_SCOUT. That
+  // saves us two thread hops, with the downside that we may underreport
+  // notifications on the first day that a user opts in to SBER_LEVEL_SCOUT.
+  report_times_.push(base::Time::Now());
+
+  BrowserThread::PostTask(
+      BrowserThread::UI, FROM_HERE,
+      base::Bind(&NotificationImageReporter::ReportNotificationImageOnUI,
+                 weak_factory_on_io_.GetWeakPtr(), profile, origin, image));
+}
+
+double NotificationImageReporter::GetReportChance() const {
+  // Get the report_chance from the Finch experiment. If there is no active
+  // experiment, it will be set to the default of 0.
+  double report_chance = variations::GetVariationParamByFeatureAsDouble(
+      kNotificationImageReporterFeature, kReportChance, 0.0);
+
+  if (report_chance < 0.0 || report_chance > 1.0) {
+    DLOG(WARNING) << "Illegal value " << report_chance << " for the parameter "
+                  << kReportChance << ". The value should be between 0 and 1.";
+    report_chance = 0.0;
+  }
+
+  return report_chance;
+}
+
+void NotificationImageReporter::SkippedReporting() {}
+
+// static
+void NotificationImageReporter::ReportNotificationImageOnUI(
+    const base::WeakPtr<NotificationImageReporter>& weak_this_on_io,
+    Profile* profile,
+    const GURL& origin,
+    const SkBitmap& image) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+  // Skip reporting unless SBER2 Scout is enabled.
+  if (GetExtendedReportingLevel(*profile->GetPrefs()) != SBER_LEVEL_SCOUT) {
+    BrowserThread::PostTask(
+        BrowserThread::IO, FROM_HERE,
+        base::Bind(&NotificationImageReporter::SkippedReporting,
+                   weak_this_on_io));
+    return;
+  }
+
+  BrowserThread::GetBlockingPool()->PostWorkerTask(
+      FROM_HERE,
+      base::Bind(
+          &NotificationImageReporter::DownscaleNotificationImageOnBlockingPool,
+          weak_this_on_io, origin, image));
+}
+
+// static
+void NotificationImageReporter::DownscaleNotificationImageOnBlockingPool(
+    const base::WeakPtr<NotificationImageReporter>& weak_this_on_io,
+    const GURL& origin,
+    const SkBitmap& image) {
+  DCHECK(BrowserThread::GetBlockingPool()->RunsTasksOnCurrentThread());
+
+  // Downscale to fit within 512x512. TODO(johnme): Get this from Finch.
+  const double MAX_SIZE = 512;
+  SkBitmap downscaled_image = image;
+  if ((image.width() > MAX_SIZE || image.height() > MAX_SIZE) &&
+      image.width() > 0 && image.height() > 0) {
+    double scale =
+        std::min(MAX_SIZE / image.width(), MAX_SIZE / image.height());
+    downscaled_image =
+        skia::ImageOperations::Resize(image, skia::ImageOperations::RESIZE_GOOD,
+                                      std::lround(scale * image.width()),
+                                      std::lround(scale * image.height()));
+  }
+
+  // Encode as PNG.
+  std::vector<unsigned char> png_bytes;
+  if (!gfx::PNGCodec::EncodeBGRASkBitmap(downscaled_image, false, &png_bytes)) {
+    NOTREACHED();
+    return;
+  }
+
+  BrowserThread::PostTask(
+      BrowserThread::IO, FROM_HERE,
+      base::Bind(&NotificationImageReporter::SendReportOnIO, weak_this_on_io,
+                 origin, base::RefCountedBytes::TakeVector(&png_bytes),
+                 gfx::Size(downscaled_image.width(), downscaled_image.height()),
+                 gfx::Size(image.width(), image.height())));
+}
+
+void NotificationImageReporter::SendReportOnIO(
+    const GURL& origin,
+    scoped_refptr<base::RefCountedMemory> data,
+    const gfx::Size& dimensions,
+    const gfx::Size& original_dimensions) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+
+  NotificationImageReportRequest report;
+  report.set_notification_origin(origin.spec());
+  report.mutable_image()->set_data(data->front(), data->size());
+  report.mutable_image()->set_mime_type(kDefaultMimeType);
+  report.mutable_image()->mutable_dimensions()->set_width(dimensions.width());
+  report.mutable_image()->mutable_dimensions()->set_height(dimensions.height());
+  if (dimensions != original_dimensions) {
+    report.mutable_image()->mutable_original_dimensions()->set_width(
+        original_dimensions.width());
+    report.mutable_image()->mutable_original_dimensions()->set_height(
+        original_dimensions.height());
+  }
+
+  std::string serialized_report;
+  report.SerializeToString(&serialized_report);
+  report_sender_->Send(
+      GURL(kReportingUploadUrl), "application/octet-stream", serialized_report,
+      base::Bind(&LogReportResult, GURL(kReportingUploadUrl), net::OK),
+      base::Bind(&LogReportResult));
+  // TODO(johnme): Consider logging bandwidth and/or duration to UMA.
+}
+
+}  // namespace safe_browsing
diff --git a/chrome/browser/safe_browsing/notification_image_reporter.h b/chrome/browser/safe_browsing/notification_image_reporter.h
new file mode 100644
index 0000000..163b4b0
--- /dev/null
+++ b/chrome/browser/safe_browsing/notification_image_reporter.h
@@ -0,0 +1,109 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SAFE_BROWSING_NOTIFICATION_IMAGE_REPORTER_H_
+#define CHROME_BROWSER_SAFE_BROWSING_NOTIFICATION_IMAGE_REPORTER_H_
+
+#include <memory>
+#include <queue>
+
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+
+class GURL;
+class Profile;
+class SkBitmap;
+
+namespace base {
+class RefCountedMemory;
+}  // namespace base
+
+namespace gfx {
+class Size;
+}  // namespace gfx
+
+namespace net {
+class ReportSender;
+class URLRequestContext;
+}  // namespace net
+
+namespace safe_browsing {
+
+class SafeBrowsingDatabaseManager;
+
+// Provides functionality for building and sending reports about notification
+// content images to the Safe Browsing CSD server.
+class NotificationImageReporter {
+ public:
+  // CSD server URL to which notification image reports are sent.
+  static const char kReportingUploadUrl[];
+
+  explicit NotificationImageReporter(net::URLRequestContext* request_context);
+  virtual ~NotificationImageReporter();
+
+  // Report notification content image to SafeBrowsing CSD server if the user
+  // has opted in, the origin is not whitelisted, and it is randomly sampled.
+  // Can only be called on IO thread.
+  void ReportNotificationImageOnIO(
+      Profile* profile,
+      const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
+      const GURL& origin,
+      const SkBitmap& image);
+
+ protected:
+  explicit NotificationImageReporter(
+      std::unique_ptr<net::ReportSender> report_sender);
+
+  // Get the percentage of images that should be reported from Finch.
+  virtual double GetReportChance() const;
+
+  // Tests may wish to override this to find out if reports are skipped. Called
+  // on the IO thread.
+  virtual void SkippedReporting();
+
+ private:
+  // Report notification content image to SafeBrowsing CSD server if necessary,
+  // by invoking DownscaleNotificationImageOnBlockingPool. This should be
+  // called on the IO thread and will invoke a member function on the IO thread
+  // using the IO-based WeapPtr.
+  static void ReportNotificationImageOnUI(
+      const base::WeakPtr<NotificationImageReporter>& weak_this_on_io,
+      Profile* profile,
+      const GURL& origin,
+      const SkBitmap& image);
+
+  // Downscales image to fit within 512x512 if necessary, and encodes as it PNG,
+  // then invokes SendReportOnIO. This should be called on a blocking pool
+  // thread and will invoke a member function on the IO thread using the
+  // IO-based WeakPtr.
+  static void DownscaleNotificationImageOnBlockingPool(
+      const base::WeakPtr<NotificationImageReporter>& weak_this_on_io,
+      const GURL& origin,
+      const SkBitmap& image);
+
+  // Serializes report using NotificationImageReportRequest protobuf defined in
+  // chrome/common/safe_browsing/csd.proto and sends it to CSD server.
+  void SendReportOnIO(const GURL& origin,
+                      scoped_refptr<base::RefCountedMemory> data,
+                      const gfx::Size& dimensions,
+                      const gfx::Size& original_dimensions);
+
+  std::unique_ptr<net::ReportSender> report_sender_;
+
+  // Timestamps of when we sent notification images. Used to limit the number
+  // of requests that we send in a day. Only access on the IO thread.
+  // TODO(johnme): Serialize this so that it doesn't reset on browser restart.
+  std::queue<base::Time> report_times_;
+
+  // Keep this last. Only dereference these pointers on the IO thread.
+  base::WeakPtrFactory<NotificationImageReporter> weak_factory_on_io_;
+
+  DISALLOW_COPY_AND_ASSIGN(NotificationImageReporter);
+};
+
+}  // namespace safe_browsing
+
+#endif  // CHROME_BROWSER_SAFE_BROWSING_NOTIFICATION_IMAGE_REPORTER_H_
diff --git a/chrome/browser/safe_browsing/notification_image_reporter_unittest.cc b/chrome/browser/safe_browsing/notification_image_reporter_unittest.cc
new file mode 100644
index 0000000..a24fcea
--- /dev/null
+++ b/chrome/browser/safe_browsing/notification_image_reporter_unittest.cc
@@ -0,0 +1,285 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/safe_browsing/notification_image_reporter.h"
+
+#include "base/callback.h"
+#include "base/memory/ptr_util.h"
+#include "base/run_loop.h"
+#include "base/test/scoped_feature_list.h"
+#include "chrome/browser/safe_browsing/mock_permission_report_sender.h"
+#include "chrome/browser/safe_browsing/ping_manager.h"
+#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
+#include "chrome/common/safe_browsing/csd.pb.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/safe_browsing_db/safe_browsing_prefs.h"
+#include "components/safe_browsing_db/test_database_manager.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/test/test_browser_thread_bundle.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "third_party/skia/include/core/SkColor.h"
+#include "url/gurl.h"
+
+using content::BrowserThread;
+
+namespace safe_browsing {
+
+namespace {
+
+class TestingNotificationImageReporter : public NotificationImageReporter {
+ public:
+  explicit TestingNotificationImageReporter(
+      std::unique_ptr<net::ReportSender> report_sender)
+      : NotificationImageReporter(std::move(report_sender)) {}
+
+  void WaitForReportSkipped() {
+    base::RunLoop run_loop;
+    quit_closure_ = run_loop.QuitClosure();
+    run_loop.Run();
+  }
+
+  void SetReportingChance(bool reporting_chance) {
+    reporting_chance_ = reporting_chance;
+  }
+
+ protected:
+  double GetReportChance() const override { return reporting_chance_; }
+  void SkippedReporting() override {
+    DCHECK_CURRENTLY_ON(BrowserThread::IO);
+    BrowserThread::PostTask(
+        BrowserThread::UI, FROM_HERE,
+        base::Bind(&TestingNotificationImageReporter::SkippedReportingOnUI,
+                   base::Unretained(this)));
+  }
+
+ private:
+  void SkippedReportingOnUI() {
+    if (quit_closure_) {
+      quit_closure_.Run();
+      quit_closure_.Reset();
+    }
+  }
+  base::Closure quit_closure_;
+  double reporting_chance_ = 1.0;
+};
+
+class FakeSafeBrowsingDatabaseManager : public TestSafeBrowsingDatabaseManager {
+ public:
+  bool MatchCsdWhitelistUrl(const GURL& url) override { return false; }
+
+ private:
+  ~FakeSafeBrowsingDatabaseManager() override {}
+};
+
+SkBitmap CreateBitmap(int width, int height) {
+  SkBitmap bitmap;
+  bitmap.allocN32Pixels(width, height);
+  bitmap.eraseColor(SK_ColorGREEN);
+  return bitmap;
+}
+
+}  // namespace
+
+class NotificationImageReporterTest : public ::testing::Test {
+ public:
+  NotificationImageReporterTest();
+
+  void SetUp() override;
+  void TearDown() override;
+
+ private:
+  content::TestBrowserThreadBundle thread_bundle_;  // Should be first member.
+
+ protected:
+  void SetExtendedReportingLevel(ExtendedReportingLevel level);
+  void ReportNotificationImage();
+
+  scoped_refptr<SafeBrowsingService> safe_browsing_service_;
+
+  std::unique_ptr<TestingProfile> profile_;  // Written on UI, read on IO.
+
+  // Owned by |notification_image_reporter_|.
+  MockPermissionReportSender* mock_report_sender_;
+
+  TestingNotificationImageReporter* notification_image_reporter_;
+
+  std::unique_ptr<base::test::ScopedFeatureList> feature_list_;
+
+  GURL origin_;     // Written on UI, read on IO.
+  SkBitmap image_;  // Written on UI, read on IO.
+
+ private:
+  void SetUpOnIO();
+
+  void ReportNotificationImageOnIO();
+};
+
+NotificationImageReporterTest::NotificationImageReporterTest()
+    // Use REAL_IO_THREAD so DCHECK_CURRENTLY_ON distinguishes IO from UI.
+    : thread_bundle_(content::TestBrowserThreadBundle::REAL_IO_THREAD),
+      origin_("https://example.com") {
+  image_ = CreateBitmap(1 /* w */, 1 /* h */);
+}
+
+void NotificationImageReporterTest::SetUp() {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+  // Initialize SafeBrowsingService with FakeSafeBrowsingDatabaseManager.
+  TestSafeBrowsingServiceFactory sb_service_factory;
+  sb_service_factory.SetTestDatabaseManager(
+      new FakeSafeBrowsingDatabaseManager());
+  SafeBrowsingService::RegisterFactory(&sb_service_factory);
+  safe_browsing_service_ = sb_service_factory.CreateSafeBrowsingService();
+  SafeBrowsingService::RegisterFactory(nullptr);
+  TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(
+      safe_browsing_service_.get());
+  g_browser_process->safe_browsing_service()->Initialize();
+  base::RunLoop().RunUntilIdle();  // TODO(johnme): Might still be tasks on IO.
+
+  profile_ = base::MakeUnique<TestingProfile>();
+
+  base::RunLoop run_loop;
+  BrowserThread::PostTaskAndReply(
+      BrowserThread::IO, FROM_HERE,
+      base::Bind(&NotificationImageReporterTest::SetUpOnIO,
+                 base::Unretained(this)),
+      run_loop.QuitClosure());
+  run_loop.Run();
+}
+
+void NotificationImageReporterTest::TearDown() {
+  TestingBrowserProcess::GetGlobal()->safe_browsing_service()->ShutDown();
+  base::RunLoop().RunUntilIdle();  // TODO(johnme): Might still be tasks on IO.
+  TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(nullptr);
+}
+
+void NotificationImageReporterTest::SetUpOnIO() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+
+  mock_report_sender_ = new MockPermissionReportSender;
+  notification_image_reporter_ = new TestingNotificationImageReporter(
+      base::WrapUnique(mock_report_sender_));
+  safe_browsing_service_->ping_manager()->notification_image_reporter_ =
+      base::WrapUnique(notification_image_reporter_);
+}
+
+void NotificationImageReporterTest::SetExtendedReportingLevel(
+    ExtendedReportingLevel level) {
+  feature_list_ = base::MakeUnique<base::test::ScopedFeatureList>();
+  if (level == SBER_LEVEL_SCOUT)
+    feature_list_->InitWithFeatures({safe_browsing::kOnlyShowScoutOptIn}, {});
+
+  InitializeSafeBrowsingPrefs(profile_->GetPrefs());
+  SetExtendedReportingPref(profile_->GetPrefs(), level != SBER_LEVEL_OFF);
+}
+
+void NotificationImageReporterTest::ReportNotificationImage() {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  BrowserThread::PostTask(
+      BrowserThread::IO, FROM_HERE,
+      base::Bind(&NotificationImageReporterTest::ReportNotificationImageOnIO,
+                 base::Unretained(this)));
+}
+
+void NotificationImageReporterTest::ReportNotificationImageOnIO() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  if (!safe_browsing_service_->enabled())
+    return;
+  safe_browsing_service_->ping_manager()->ReportNotificationImage(
+      profile_.get(), safe_browsing_service_->database_manager(), origin_,
+      image_);
+}
+
+TEST_F(NotificationImageReporterTest, ReportSuccess) {
+  SetExtendedReportingLevel(SBER_LEVEL_SCOUT);
+
+  ReportNotificationImage();
+  mock_report_sender_->WaitForReportSent();
+
+  ASSERT_EQ(1, mock_report_sender_->GetAndResetNumberOfReportsSent());
+  EXPECT_EQ(GURL(NotificationImageReporter::kReportingUploadUrl),
+            mock_report_sender_->latest_report_uri());
+  EXPECT_EQ("application/octet-stream",
+            mock_report_sender_->latest_content_type());
+
+  NotificationImageReportRequest report;
+  ASSERT_TRUE(report.ParseFromString(mock_report_sender_->latest_report()));
+  EXPECT_EQ(origin_.spec(), report.notification_origin());
+  ASSERT_TRUE(report.has_image());
+  EXPECT_GT(report.image().data().size(), 0U);
+  ASSERT_TRUE(report.image().has_mime_type());
+  EXPECT_EQ(report.image().mime_type(), "image/png");
+  ASSERT_TRUE(report.image().has_dimensions());
+  EXPECT_EQ(1, report.image().dimensions().width());
+  EXPECT_EQ(1, report.image().dimensions().height());
+  EXPECT_FALSE(report.image().has_original_dimensions());
+}
+
+TEST_F(NotificationImageReporterTest, ImageDownscaling) {
+  SetExtendedReportingLevel(SBER_LEVEL_SCOUT);
+
+  image_ = CreateBitmap(640 /* w */, 360 /* h */);
+
+  ReportNotificationImage();
+  mock_report_sender_->WaitForReportSent();
+
+  NotificationImageReportRequest report;
+  ASSERT_TRUE(report.ParseFromString(mock_report_sender_->latest_report()));
+  ASSERT_TRUE(report.has_image());
+  EXPECT_GT(report.image().data().size(), 0U);
+  ASSERT_TRUE(report.image().has_dimensions());
+  EXPECT_EQ(512, report.image().dimensions().width());
+  EXPECT_EQ(288, report.image().dimensions().height());
+  ASSERT_TRUE(report.image().has_original_dimensions());
+  EXPECT_EQ(640, report.image().original_dimensions().width());
+  EXPECT_EQ(360, report.image().original_dimensions().height());
+}
+
+TEST_F(NotificationImageReporterTest, NoReportWithoutSBER) {
+  SetExtendedReportingLevel(SBER_LEVEL_OFF);
+
+  ReportNotificationImage();
+  notification_image_reporter_->WaitForReportSkipped();
+
+  EXPECT_EQ(0, mock_report_sender_->GetAndResetNumberOfReportsSent());
+}
+
+TEST_F(NotificationImageReporterTest, NoReportWithoutScout) {
+  SetExtendedReportingLevel(SBER_LEVEL_LEGACY);
+
+  ReportNotificationImage();
+  notification_image_reporter_->WaitForReportSkipped();
+
+  EXPECT_EQ(0, mock_report_sender_->GetAndResetNumberOfReportsSent());
+}
+
+TEST_F(NotificationImageReporterTest, NoReportWithoutReportingEnabled) {
+  SetExtendedReportingLevel(SBER_LEVEL_SCOUT);
+  notification_image_reporter_->SetReportingChance(0.0);
+
+  ReportNotificationImage();
+  notification_image_reporter_->WaitForReportSkipped();
+
+  EXPECT_EQ(0, mock_report_sender_->GetAndResetNumberOfReportsSent());
+}
+
+TEST_F(NotificationImageReporterTest, MaxReportsPerDay) {
+  SetExtendedReportingLevel(SBER_LEVEL_SCOUT);
+
+  const int kMaxReportsPerDay = 5;
+
+  for (int i = 0; i < kMaxReportsPerDay; i++) {
+    ReportNotificationImage();
+    mock_report_sender_->WaitForReportSent();
+  }
+  ReportNotificationImage();
+  notification_image_reporter_->WaitForReportSkipped();
+
+  EXPECT_EQ(kMaxReportsPerDay,
+            mock_report_sender_->GetAndResetNumberOfReportsSent());
+}
+
+}  // namespace safe_browsing
diff --git a/chrome/browser/safe_browsing/ping_manager.cc b/chrome/browser/safe_browsing/ping_manager.cc
index 2a42e9b..b223a15f 100644
--- a/chrome/browser/safe_browsing/ping_manager.cc
+++ b/chrome/browser/safe_browsing/ping_manager.cc
@@ -12,6 +12,7 @@
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "base/values.h"
+#include "chrome/browser/safe_browsing/notification_image_reporter.h"
 #include "chrome/browser/safe_browsing/permission_reporter.h"
 #include "components/data_use_measurement/core/data_use_user_data.h"
 #include "content/public/browser/browser_thread.h"
@@ -24,6 +25,7 @@
 #include "net/url_request/url_request_context.h"
 #include "net/url_request/url_request_context_getter.h"
 #include "net/url_request/url_request_status.h"
+#include "third_party/skia/include/core/SkBitmap.h"
 #include "url/gurl.h"
 
 using content::BrowserThread;
@@ -82,8 +84,10 @@
   DCHECK(!url_prefix_.empty());
 
   if (request_context_getter) {
-    permission_reporter_.reset(
-        new PermissionReporter(request_context_getter->GetURLRequestContext()));
+    permission_reporter_ = base::MakeUnique<PermissionReporter>(
+        request_context_getter->GetURLRequestContext());
+    notification_image_reporter_ = base::MakeUnique<NotificationImageReporter>(
+        request_context_getter->GetURLRequestContext());
 
     net_log_ = net::NetLogWithSource::Make(
         request_context_getter->GetURLRequestContext()->net_log(),
@@ -170,6 +174,15 @@
   permission_reporter_->SendReport(report_info);
 }
 
+void SafeBrowsingPingManager::ReportNotificationImage(
+    Profile* profile,
+    const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
+    const GURL& origin,
+    const SkBitmap& image) {
+  notification_image_reporter_->ReportNotificationImageOnIO(
+      profile, database_manager, origin, image);
+}
+
 GURL SafeBrowsingPingManager::SafeBrowsingHitUrl(
     const safe_browsing::HitReport& hit_report) const {
   DCHECK(hit_report.threat_type == SB_THREAT_TYPE_URL_MALWARE ||
diff --git a/chrome/browser/safe_browsing/ping_manager.h b/chrome/browser/safe_browsing/ping_manager.h
index 9f0c1977..8f5e1dd 100644
--- a/chrome/browser/safe_browsing/ping_manager.h
+++ b/chrome/browser/safe_browsing/ping_manager.h
@@ -23,13 +23,18 @@
 #include "net/url_request/url_fetcher_delegate.h"
 #include "url/gurl.h"
 
+class Profile;
+class SkBitmap;
+
 namespace net {
 class URLRequestContextGetter;
 }  // namespace net
 
 namespace safe_browsing {
 
+class NotificationImageReporter;
 class PermissionReporter;
+class SafeBrowsingDatabaseManager;
 
 class SafeBrowsingPingManager : public net::URLFetcherDelegate {
  public:
@@ -55,7 +60,15 @@
   // Report permission action to SafeBrowsing servers.
   void ReportPermissionAction(const PermissionReportInfo& report_info);
 
+  // Report notification content image to SafeBrowsing CSD server if necessary.
+  void ReportNotificationImage(
+      Profile* profile,
+      const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
+      const GURL& origin,
+      const SkBitmap& image);
+
  private:
+  friend class NotificationImageReporterTest;
   friend class PermissionReporterBrowserTest;
   friend class SafeBrowsingPingManagerTest;
   FRIEND_TEST_ALL_PREFIXES(SafeBrowsingPingManagerTest,
@@ -102,6 +115,9 @@
   // Sends reports of permission actions.
   std::unique_ptr<PermissionReporter> permission_reporter_;
 
+  // Sends reports of notification content images.
+  std::unique_ptr<NotificationImageReporter> notification_image_reporter_;
+
   net::NetLogWithSource net_log_;
 
   DISALLOW_COPY_AND_ASSIGN(SafeBrowsingPingManager);