commit 4ad63eb3aa65ce7baa08190aac2770540dc25f43
Author: Greg Stoll <gstoll@mozilla.com>
Date: Wed, 27 Mar 2024 12:13:56 -0500

    Mozilla improvements to content_analysis_sdk

    - add ability for demo agent to block/warn/report specific regexes
    - add ability for demo agent to chose a sequence of delays to apply

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 39477223f031c..5dacc81031117 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -203,6 +203,7 @@ add_executable(agent
   ./demo/agent.cc
   ./demo/handler.h
 )
+target_compile_features(agent PRIVATE cxx_std_17)
 target_include_directories(agent PRIVATE ${AGENT_INCLUDES})
 target_link_libraries(agent PRIVATE cac_agent)
 
diff --git a/agent/src/event_win.h b/agent/src/event_win.h
index 9f8b6903566f2..f631f693dcd9c 100644
--- a/agent/src/event_win.h
+++ b/agent/src/event_win.h
@@ -28,6 +28,12 @@ class ContentAnalysisEventWin : public ContentAnalysisEventBase {
   ResultCode Close() override;
   ResultCode Send() override;
   std::string DebugString() const override;
+  std::string SerializeStringToSendToBrowser() {
+    return agent_to_chrome()->SerializeAsString();
+  }
+  void SetResponseSent() { response_sent_ = true; }
+  
+  HANDLE Pipe() const { return hPipe_; }
 
  private:
   void Shutdown();
diff --git a/browser/src/client_win.cc b/browser/src/client_win.cc
index 9d3d7e8c52662..039946d131398 100644
--- a/browser/src/client_win.cc
+++ b/browser/src/client_win.cc
@@ -418,7 +418,11 @@ DWORD ClientWin::ConnectToPipe(const std::string& pipename, HANDLE* handle) {
 
 void ClientWin::Shutdown() {
   if (hPipe_ != INVALID_HANDLE_VALUE) {
-    FlushFileBuffers(hPipe_);
+    // TODO: This trips the LateWriteObserver.  We could move this earlier
+    // (before the LateWriteObserver is created) or just remove it, although
+    // the later could mean an ACK message is not processed by the agent
+    // in time.
+    // FlushFileBuffers(hPipe_);
     CloseHandle(hPipe_);
     hPipe_ = INVALID_HANDLE_VALUE;
   }
diff --git a/demo/agent.cc b/demo/agent.cc
index ff8b93f647ebd..3e168b0915a0c 100644
--- a/demo/agent.cc
+++ b/demo/agent.cc
@@ -2,12 +2,18 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <algorithm>
 #include <fstream>
 #include <iostream>
 #include <string>
+#include <regex>
+#include <vector>
 
 #include "content_analysis/sdk/analysis_agent.h"
 #include "demo/handler.h"
+#include "demo/handler_misbehaving.h"
+
+using namespace content_analysis::sdk;
 
 // Different paths are used depending on whether this agent should run as a
 // use specific agent or not.  These values are chosen to match the test
@@ -19,19 +25,50 @@ constexpr char kPathSystem[] = "brcm_chrm_cas";
 std::string path = kPathSystem;
 bool use_queue = false;
 bool user_specific = false;
-unsigned long delay = 0;  // In seconds.
+std::vector<unsigned long> delays = {0};  // In seconds.
 unsigned long num_threads = 8u;
 std::string save_print_data_path = "";
+RegexArray toBlock, toWarn, toReport;
+static bool useMisbehavingHandler = false;
+static std::string modeStr;
 
 // Command line parameters.
-constexpr const char* kArgDelaySpecific = "--delay=";
+constexpr const char* kArgDelaySpecific = "--delays=";
 constexpr const char* kArgPath = "--path=";
 constexpr const char* kArgQueued = "--queued";
 constexpr const char* kArgThreads = "--threads=";
 constexpr const char* kArgUserSpecific = "--user";
+constexpr const char* kArgToBlock = "--toblock=";
+constexpr const char* kArgToWarn = "--towarn=";
+constexpr const char* kArgToReport = "--toreport=";
+constexpr const char* kArgMisbehave = "--misbehave=";
 constexpr const char* kArgHelp = "--help";
 constexpr const char* kArgSavePrintRequestDataTo = "--save-print-request-data-to=";
 
+std::map<std::string, Mode> sStringToMode = {
+#define AGENT_MODE(name) {#name, Mode::Mode_##name},
+#include "modes.h"
+#undef AGENT_MODE
+};
+
+std::map<Mode, std::string> sModeToString = {
+#define AGENT_MODE(name) {Mode::Mode_##name, #name},
+#include "modes.h"
+#undef AGENT_MODE
+};
+
+std::vector<std::pair<std::string, std::regex>>
+ParseRegex(const std::string str) {
+  std::vector<std::pair<std::string, std::regex>> ret;
+  for (auto it = str.begin(); it != str.end(); /* nop */) {
+    auto it2 = std::find(it, str.end(), ',');
+    ret.push_back(std::make_pair(std::string(it, it2), std::regex(it, it2)));
+    it = it2 == str.end() ? it2 : it2 + 1;
+  }
+
+  return ret;
+}
+
 bool ParseCommandLine(int argc, char* argv[]) {
   for (int i = 1; i < argc; ++i) {
     const std::string arg = argv[i];
@@ -44,16 +81,38 @@ bool ParseCommandLine(int argc, char* argv[]) {
       path = kPathUser;
       user_specific = true;
     } else if (arg.find(kArgDelaySpecific) == 0) {
-      delay = std::stoul(arg.substr(strlen(kArgDelaySpecific)));
+      std::string delaysStr = arg.substr(strlen(kArgDelaySpecific));
+      delays.clear();
+      size_t posStart = 0, posEnd;
+      unsigned long delay;
+      while ((posEnd = delaysStr.find(',', posStart)) != std::string::npos) {
+        delay = std::stoul(delaysStr.substr(posStart, posEnd - posStart));
+        if (delay > 30) {
+            delay = 30;
+        }
+        delays.push_back(delay);
+        posStart = posEnd + 1;
+      }
+      delay = std::stoul(delaysStr.substr(posStart));
       if (delay > 30) {
           delay = 30;
       }
+      delays.push_back(delay);
     } else if (arg.find(kArgPath) == 0) {
       path = arg.substr(strlen(kArgPath));
     } else if (arg.find(kArgQueued) == 0) {
       use_queue = true;
     } else if (arg.find(kArgThreads) == 0) {
       num_threads = std::stoul(arg.substr(strlen(kArgThreads)));
+    } else if (arg.find(kArgToBlock) == 0) {
+      toBlock = ParseRegex(arg.substr(strlen(kArgToBlock)));
+    } else if (arg.find(kArgToWarn) == 0) {
+      toWarn = ParseRegex(arg.substr(strlen(kArgToWarn)));
+    } else if (arg.find(kArgToReport) == 0) {
+      toReport = ParseRegex(arg.substr(strlen(kArgToReport)));
+    } else if (arg.find(kArgMisbehave) == 0) {
+      modeStr = arg.substr(strlen(kArgMisbehave));
+      useMisbehavingHandler = true;
     } else if (arg.find(kArgHelp) == 0) {
       return false;
     } else if (arg.find(kArgSavePrintRequestDataTo) == 0) {
@@ -72,13 +131,17 @@ void PrintHelp() {
     << "A simple agent to process content analysis requests." << std::endl
     << "Data containing the string 'block' blocks the request data from being used." << std::endl
     << std::endl << "Options:"  << std::endl
-    << kArgDelaySpecific << "<delay> : Add a delay to request processing in seconds (max 30)." << std::endl
+    << kArgDelaySpecific << "<delay1,delay2,...> : Add delays to request processing in seconds. Delays are limited to 30 seconds and are applied round-robin to requests. Default is 0." << std::endl
     << kArgPath << " <path> : Used the specified path instead of default. Must come after --user." << std::endl
     << kArgQueued << " : Queue requests for processing in a background thread" << std::endl
     << kArgThreads << " : When queued, number of threads in the request processing thread pool" << std::endl
     << kArgUserSpecific << " : Make agent OS user specific." << std::endl
     << kArgHelp << " : prints this help message" << std::endl
-    << kArgSavePrintRequestDataTo << " : saves the PDF data to the given file path for print requests";
+    << kArgSavePrintRequestDataTo << " : saves the PDF data to the given file path for print requests" << std::endl
+    << kArgToBlock << "<regex> : Regular expression matching file and text content to block." << std::endl
+    << kArgToWarn << "<regex> : Regular expression matching file and text content to warn about." << std::endl
+    << kArgToReport << "<regex> : Regular expression matching file and text content to report." << std::endl
+    << kArgMisbehave << "<mode> : Use 'misbehaving' agent in given mode for testing purposes." << std::endl;
 }
 
 int main(int argc, char* argv[]) {
@@ -87,9 +150,17 @@ int main(int argc, char* argv[]) {
     return 1;
   }
 
-  auto handler = use_queue
-      ? std::make_unique<QueuingHandler>(num_threads, delay, save_print_data_path)
-      : std::make_unique<Handler>(delay, save_print_data_path);
+  auto handler =
+    useMisbehavingHandler
+      ? MisbehavingHandler::Create(modeStr, std::move(delays), save_print_data_path, std::move(toBlock), std::move(toWarn), std::move(toReport))
+      : use_queue
+        ? std::make_unique<QueuingHandler>(num_threads, std::move(delays), save_print_data_path, std::move(toBlock), std::move(toWarn), std::move(toReport))
+        : std::make_unique<Handler>(std::move(delays), save_print_data_path, std::move(toBlock), std::move(toWarn), std::move(toReport));
+
+  if (!handler) {
+    std::cout << "[Demo] Failed to construct handler." << std::endl;
+    return 1;
+  }
 
   // Each agent uses a unique name to identify itself with Google Chrome.
   content_analysis::sdk::ResultCode rc;
diff --git a/demo/handler.h b/demo/handler.h
index 9d1ccfdf9857a..88599963c51b0 100644
--- a/demo/handler.h
+++ b/demo/handler.h
@@ -7,31 +7,51 @@
 
 #include <time.h>
 
+#include <algorithm>
+#include <atomic>
 #include <chrono>
 #include <cstdio>
 #include <fstream>
 #include <iostream>
+#include <optional>
 #include <thread>
 #include <utility>
+#include <regex>
 #include <vector>
 
 #include "content_analysis/sdk/analysis_agent.h"
 #include "demo/atomic_output.h"
 #include "demo/request_queue.h"
 
+using RegexArray = std::vector<std::pair<std::string, std::regex>>;
+
 // An AgentEventHandler that dumps requests information to stdout and blocks
 // any requests that have the keyword "block" in their data
 class Handler : public content_analysis::sdk::AgentEventHandler {
  public:
   using Event = content_analysis::sdk::ContentAnalysisEvent;
 
-  Handler(unsigned long delay, const std::string& print_data_file_path) :
-      delay_(delay), print_data_file_path_(print_data_file_path) {
-  }
+  Handler(std::vector<unsigned long>&& delays, const std::string& print_data_file_path,
+          RegexArray&& toBlock = RegexArray(),
+          RegexArray&& toWarn = RegexArray(),
+          RegexArray&& toReport = RegexArray()) :
+      toBlock_(std::move(toBlock)), toWarn_(std::move(toWarn)), toReport_(std::move(toReport)),
+      delays_(std::move(delays)), print_data_file_path_(print_data_file_path) {}
 
-  unsigned long delay() { return delay_; }
+  const std::vector<unsigned long> delays() { return delays_; }
+  size_t nextDelayIndex() const { return nextDelayIndex_; }
 
  protected:
+  // subclasses can override this
+  // returns whether the response has been set
+  virtual bool SetCustomResponse(AtomicCout& aout, std::unique_ptr<Event>& event) {
+    return false;
+  }
+  // subclasses can override this
+  // returns whether the response has been sent
+  virtual bool SendCustomResponse(std::unique_ptr<Event>& event) {
+    return false;
+  }
   // Analyzes one request from Google Chrome and responds back to the browser
   // with either an allow or block verdict.
   void AnalyzeContent(AtomicCout& aout, std::unique_ptr<Event> event) {
@@ -43,29 +63,25 @@ class Handler : public content_analysis::sdk::AgentEventHandler {
 
     DumpEvent(aout.stream(), event.get());
 
-    bool block = false;
     bool success = true;
-    unsigned long delay = delay_;
-
-    if (event->GetRequest().has_text_content()) {
-      block = ShouldBlockRequest(
-          event->GetRequest().text_content());
-      GetFileSpecificDelay(event->GetRequest().text_content(), &delay);
-    } else if (event->GetRequest().has_file_path()) {
-      std::string content;
-      success =
-          ReadContentFromFile(event->GetRequest().file_path(),
-                              &content);
-      if (success) {
-        block = ShouldBlockRequest(content);
-        GetFileSpecificDelay(content, &delay);
+    std::optional<content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action> caResponse;
+    bool setResponse = SetCustomResponse(aout, event);
+    if (!setResponse) {
+      caResponse = content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK;
+      if (event->GetRequest().has_text_content()) {
+        caResponse = DecideCAResponse(
+            event->GetRequest().text_content(), aout.stream());
+      } else if (event->GetRequest().has_file_path()) {
+        // TODO: Fix downloads to store file *first* so we can check contents.
+        // Until then, just check the file name:
+        caResponse = DecideCAResponse(
+            event->GetRequest().file_path(), aout.stream());
+      } else if (event->GetRequest().has_print_data()) {
+        // In the case of print request, normally the PDF bytes would be parsed
+        // for sensitive data violations. To keep this class simple, only the
+        // URL is checked for the word "block".
+        caResponse = DecideCAResponse(event->GetRequest().request_data().url(), aout.stream());
       }
-    } else if (event->GetRequest().has_print_data()) {
-      // In the case of print request, normally the PDF bytes would be parsed
-      // for sensitive data violations. To keep this class simple, only the
-      // URL is checked for the word "block".
-      block = ShouldBlockRequest(event->GetRequest().request_data().url());
-      GetFileSpecificDelay(event->GetRequest().request_data().url(), &delay);
     }
 
     if (!success) {
@@ -75,22 +91,44 @@ class Handler : public content_analysis::sdk::AgentEventHandler {
           content_analysis::sdk::ContentAnalysisResponse::Result::FAILURE);
       aout.stream() << "  Verdict: failed to reach verdict: ";
       aout.stream() << event->DebugString() << std::endl;
-    } else if (block) {
-      auto rc = content_analysis::sdk::SetEventVerdictToBlock(event.get());
-      aout.stream() << "  Verdict: block";
-      if (rc != content_analysis::sdk::ResultCode::OK) {
-        aout.stream() << " error: "
-                      << content_analysis::sdk::ResultCodeToString(rc) << std::endl;
-        aout.stream() << "  " << event->DebugString() << std::endl;
+    } else {
+      aout.stream() << "  Verdict: ";
+      if (caResponse) {
+        switch (caResponse.value()) {
+          case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK:
+            aout.stream() << "BLOCK";
+            break;
+          case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_WARN:
+            aout.stream() << "WARN";
+            break;
+          case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_REPORT_ONLY:
+            aout.stream() << "REPORT_ONLY";
+            break;
+          case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_ACTION_UNSPECIFIED:
+            aout.stream() << "ACTION_UNSPECIFIED";
+            break;
+          default:
+            aout.stream() << "<error>";
+            break;
+        }
+        auto rc =
+          content_analysis::sdk::SetEventVerdictTo(event.get(), caResponse.value());
+        if (rc != content_analysis::sdk::ResultCode::OK) {
+          aout.stream() << " error: "
+                        << content_analysis::sdk::ResultCodeToString(rc) << std::endl;
+          aout.stream() << "  " << event->DebugString() << std::endl;
+        }
+        aout.stream() << std::endl;
+      } else {
+        aout.stream() << "  Verdict: allow" << std::endl;
       }
       aout.stream() << std::endl;
-    } else {
-      aout.stream() << "  Verdict: allow" << std::endl;
     }
-
     aout.stream() << std::endl;
 
     // If a delay is specified, wait that much.
+    size_t nextDelayIndex = nextDelayIndex_.fetch_add(1);
+    unsigned long delay = delays_[nextDelayIndex % delays_.size()];
     if (delay > 0) {
       aout.stream() << "Delaying response to " << event->GetRequest().request_token()
                     << " for " << delay << "s" << std::endl<< std::endl;
@@ -99,16 +137,19 @@ class Handler : public content_analysis::sdk::AgentEventHandler {
     }
 
     // Send the response back to Google Chrome.
-    auto rc = event->Send();
-    if (rc != content_analysis::sdk::ResultCode::OK) {
-      aout.stream() << "[Demo] Error sending response: "
-                    << content_analysis::sdk::ResultCodeToString(rc)
-                    << std::endl;
-      aout.stream() << event->DebugString() << std::endl;
+    bool sentCustomResponse = SendCustomResponse(event);
+    if (!sentCustomResponse) {
+      auto rc = event->Send();
+      if (rc != content_analysis::sdk::ResultCode::OK) {
+        aout.stream() << "[Demo] Error sending response: "
+                      << content_analysis::sdk::ResultCodeToString(rc)
+                      << std::endl;
+        aout.stream() << event->DebugString() << std::endl;
+      }
     }
   }
 
- private:
+ protected:
   void OnBrowserConnected(
       const content_analysis::sdk::BrowserInfo& info) override {
     AtomicCout aout;
@@ -362,21 +403,40 @@ class Handler : public content_analysis::sdk::AgentEventHandler {
     return true;
   }
 
-  bool ShouldBlockRequest(const std::string& content) {
-    // Determines if the request should be blocked.  For this simple example
-    // the content is blocked if the string "block" is found.  Otherwise the
-    // content is allowed.
-    return content.find("block") != std::string::npos;
-  }
-
-  void GetFileSpecificDelay(const std::string& content, unsigned long* delay) {
-    auto pos = content.find("delay=");
-    if (pos != std::string::npos) {
-      std::sscanf(content.substr(pos).c_str(), "delay=%lu", delay);
+  std::optional<content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action>
+  DecideCAResponse(const std::string& content, std::stringstream& stream) {
+    for (auto& r : toBlock_) {
+      if (std::regex_search(content, r.second)) {
+        stream << "'" << content << "' matches BLOCK regex '"
+                  << r.first << "'" << std::endl;
+        return content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK;
+      }
     }
+    for (auto& r : toWarn_) {
+      if (std::regex_search(content, r.second)) {
+        stream << "'" << content << "' matches WARN regex '"
+                  << r.first << "'" << std::endl;
+        return content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_WARN;
+      }
+    }
+    for (auto& r : toReport_) {
+      if (std::regex_search(content, r.second)) {
+        stream << "'" << content << "' matches REPORT_ONLY regex '"
+                  << r.first << "'" << std::endl;
+        return content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_REPORT_ONLY;
+      }
+    }
+    stream << "'" << content << "' was ALLOWed\n";
+    return {};
   }
 
-  unsigned long delay_;
+  // For the demo, block any content that matches these wildcards.
+  RegexArray toBlock_;
+  RegexArray toWarn_;
+  RegexArray toReport_;
+
+  std::vector<unsigned long> delays_;
+  std::atomic<size_t> nextDelayIndex_;
   std::string print_data_file_path_;
 };
 
@@ -384,8 +444,11 @@ class Handler : public content_analysis::sdk::AgentEventHandler {
 // any requests that have the keyword "block" in their data
 class QueuingHandler : public Handler {
  public:
-  QueuingHandler(unsigned long threads, unsigned long delay, const std::string& print_data_file_path)
-      : Handler(delay, print_data_file_path)  {
+  QueuingHandler(unsigned long threads, std::vector<unsigned long>&& delays, const std::string& print_data_file_path,
+    RegexArray&& toBlock = RegexArray(),
+          RegexArray&& toWarn = RegexArray(),
+          RegexArray&& toReport = RegexArray())
+      : Handler(std::move(delays), print_data_file_path, std::move(toBlock), std::move(toWarn), std::move(toReport))  {
     StartBackgroundThreads(threads);
   }
 
@@ -421,6 +484,8 @@ class QueuingHandler : public Handler {
       aout.stream()  << std::endl << "----------" << std::endl;
       aout.stream() << "Thread: " << std::this_thread::get_id()
                     << std::endl;
+      aout.stream() << "Delaying request processing for "
+                    << handler->delays()[handler->nextDelayIndex() % handler->delays().size()] << "s" << std::endl << std::endl;
       aout.flush();
 
       handler->AnalyzeContent(aout, std::move(event));
-- 
2.42.0.windows.2

