411 lines
14 KiB
C++
411 lines
14 KiB
C++
// Copyright 2022 The Chromium Authors.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include <time.h>
|
|
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include "content_analysis/sdk/analysis_client.h"
|
|
#include "demo/atomic_output.h"
|
|
|
|
using content_analysis::sdk::Client;
|
|
using content_analysis::sdk::ContentAnalysisRequest;
|
|
using content_analysis::sdk::ContentAnalysisResponse;
|
|
using content_analysis::sdk::ContentAnalysisAcknowledgement;
|
|
|
|
// 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
|
|
// values in chrome browser.
|
|
constexpr char kPathUser[] = "path_user";
|
|
constexpr char kPathSystem[] = "brcm_chrm_cas";
|
|
|
|
// Global app config.
|
|
std::string path = kPathSystem;
|
|
bool user_specific = false;
|
|
bool group = false;
|
|
std::unique_ptr<Client> client;
|
|
|
|
// Paramters used to build the request.
|
|
content_analysis::sdk::AnalysisConnector connector =
|
|
content_analysis::sdk::FILE_ATTACHED;
|
|
time_t request_token_number = time(nullptr);
|
|
std::string request_token;
|
|
std::string tag = "dlp";
|
|
bool threaded = false;
|
|
std::string digest = "sha256-123456";
|
|
std::string url = "https://upload.example.com";
|
|
std::string email = "me@example.com";
|
|
std::string machine_user = "DOMAIN\\me";
|
|
std::vector<std::string> datas;
|
|
|
|
// When grouping, remember the tokens of all requests/responses in order to
|
|
// acknowledge them all with the same final action.
|
|
//
|
|
// This global state. It may be access from multiple thread so must be
|
|
// accessed from a critical section.
|
|
std::mutex global_mutex;
|
|
ContentAnalysisAcknowledgement::FinalAction global_final_action =
|
|
ContentAnalysisAcknowledgement::ALLOW;
|
|
std::vector<std::string> request_tokens;
|
|
|
|
// Command line parameters.
|
|
constexpr const char* kArgConnector = "--connector=";
|
|
constexpr const char* kArgDigest = "--digest=";
|
|
constexpr const char* kArgEmail = "--email=";
|
|
constexpr const char* kArgGroup = "--group";
|
|
constexpr const char* kArgMachineUser = "--machine-user=";
|
|
constexpr const char* kArgPath = "--path=";
|
|
constexpr const char* kArgRequestToken = "--request-token=";
|
|
constexpr const char* kArgTag = "--tag=";
|
|
constexpr const char* kArgThreaded = "--threaded";
|
|
constexpr const char* kArgUrl = "--url=";
|
|
constexpr const char* kArgUserSpecific = "--user";
|
|
constexpr const char* kArgHelp = "--help";
|
|
|
|
bool ParseCommandLine(int argc, char* argv[]) {
|
|
for (int i = 1; i < argc; ++i) {
|
|
const std::string arg = argv[i];
|
|
if (arg.find(kArgConnector) == 0) {
|
|
std::string connector_str = arg.substr(strlen(kArgConnector));
|
|
if (connector_str == "download") {
|
|
connector = content_analysis::sdk::FILE_DOWNLOADED;
|
|
} else if (connector_str == "attach") {
|
|
connector = content_analysis::sdk::FILE_ATTACHED;
|
|
} else if (connector_str == "bulk-data-entry") {
|
|
connector = content_analysis::sdk::BULK_DATA_ENTRY;
|
|
} else if (connector_str == "print") {
|
|
connector = content_analysis::sdk::PRINT;
|
|
} else if (connector_str == "file-transfer") {
|
|
connector = content_analysis::sdk::FILE_TRANSFER;
|
|
} else {
|
|
std::cout << "[Demo] Incorrect command line arg: " << arg << std::endl;
|
|
return false;
|
|
}
|
|
} else if (arg.find(kArgRequestToken) == 0) {
|
|
request_token = arg.substr(strlen(kArgRequestToken));
|
|
} else if (arg.find(kArgTag) == 0) {
|
|
tag = arg.substr(strlen(kArgTag));
|
|
} else if (arg.find(kArgThreaded) == 0) {
|
|
threaded = true;
|
|
} else if (arg.find(kArgDigest) == 0) {
|
|
digest = arg.substr(strlen(kArgDigest));
|
|
} else if (arg.find(kArgUrl) == 0) {
|
|
url = arg.substr(strlen(kArgUrl));
|
|
} else if (arg.find(kArgMachineUser) == 0) {
|
|
machine_user = arg.substr(strlen(kArgMachineUser));
|
|
} else if (arg.find(kArgEmail) == 0) {
|
|
email = arg.substr(strlen(kArgEmail));
|
|
} else if (arg.find(kArgPath) == 0) {
|
|
path = arg.substr(strlen(kArgPath));
|
|
} else if (arg.find(kArgUserSpecific) == 0) {
|
|
// If kArgPath was already used, abort.
|
|
if (path != kPathSystem) {
|
|
std::cout << std::endl << "ERROR: use --path=<path> after --user";
|
|
return false;
|
|
}
|
|
path = kPathUser;
|
|
user_specific = true;
|
|
} else if (arg.find(kArgGroup) == 0) {
|
|
group = true;
|
|
} else if (arg.find(kArgHelp) == 0) {
|
|
return false;
|
|
} else {
|
|
datas.push_back(arg);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PrintHelp() {
|
|
std::cout
|
|
<< std::endl << std::endl
|
|
<< "Usage: client [OPTIONS] [@]content_or_file ..." << std::endl
|
|
<< "A simple client to send content analysis requests to a running agent." << std::endl
|
|
<< "Without @ the content to analyze is the argument itself." << std::endl
|
|
<< "Otherwise the content is read from a file called 'content_or_file'." << std::endl
|
|
<< "Multiple [@]content_or_file arguments may be specified, each generates one request." << std::endl
|
|
<< std::endl << "Options:" << std::endl
|
|
<< kArgConnector << "<connector> : one of 'download', 'attach' (default), 'bulk-data-entry', 'print', or 'file-transfer'" << std::endl
|
|
<< kArgRequestToken << "<unique-token> : defaults to 'req-<number>' which auto increments" << std::endl
|
|
<< kArgTag << "<tag> : defaults to 'dlp'" << std::endl
|
|
<< kArgThreaded << " : handled multiple requests using threads" << std::endl
|
|
<< kArgUrl << "<url> : defaults to 'https://upload.example.com'" << std::endl
|
|
<< kArgMachineUser << "<machine-user> : defaults to 'DOMAIN\\me'" << std::endl
|
|
<< kArgEmail << "<email> : defaults to 'me@example.com'" << std::endl
|
|
<< kArgPath << " <path> : Used the specified path instead of default. Must come after --user." << std::endl
|
|
<< kArgUserSpecific << " : Connects to an OS user specific agent" << std::endl
|
|
<< kArgDigest << "<digest> : defaults to 'sha256-123456'" << std::endl
|
|
<< kArgGroup << " : Generate the same final action for all requests" << std::endl
|
|
<< kArgHelp << " : prints this help message" << std::endl;
|
|
}
|
|
|
|
std::string GenerateRequestToken() {
|
|
std::stringstream stm;
|
|
stm << "req-" << request_token_number++;
|
|
return stm.str();
|
|
}
|
|
|
|
ContentAnalysisRequest BuildRequest(const std::string& data) {
|
|
std::string filepath;
|
|
std::string filename;
|
|
if (data[0] == '@') {
|
|
filepath = data.substr(1);
|
|
filename = filepath.substr(filepath.find_last_of("/\\") + 1);
|
|
}
|
|
|
|
ContentAnalysisRequest request;
|
|
|
|
// Set request to expire 5 minutes into the future.
|
|
request.set_expires_at(time(nullptr) + 5 * 60);
|
|
request.set_analysis_connector(connector);
|
|
request.set_request_token(!request_token.empty()
|
|
? request_token : GenerateRequestToken());
|
|
*request.add_tags() = tag;
|
|
|
|
auto request_data = request.mutable_request_data();
|
|
request_data->set_url(url);
|
|
request_data->set_email(email);
|
|
request_data->set_digest(digest);
|
|
if (!filename.empty()) {
|
|
request_data->set_filename(filename);
|
|
}
|
|
|
|
auto client_metadata = request.mutable_client_metadata();
|
|
auto browser = client_metadata->mutable_browser();
|
|
browser->set_machine_user(machine_user);
|
|
|
|
if (!filepath.empty()) {
|
|
request.set_file_path(filepath);
|
|
} else if (!data.empty()) {
|
|
request.set_text_content(data);
|
|
} else {
|
|
std::cout << "[Demo] Specify text content or a file path." << std::endl;
|
|
PrintHelp();
|
|
exit(1);
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
// Gets the most severe action within the result.
|
|
ContentAnalysisResponse::Result::TriggeredRule::Action
|
|
GetActionFromResult(const ContentAnalysisResponse::Result& result) {
|
|
auto action =
|
|
ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED;
|
|
for (auto rule : result.triggered_rules()) {
|
|
if (rule.has_action() && rule.action() > action)
|
|
action = rule.action();
|
|
}
|
|
return action;
|
|
}
|
|
|
|
// Gets the most severe action within all the the results of a response.
|
|
ContentAnalysisResponse::Result::TriggeredRule::Action
|
|
GetActionFromResponse(const ContentAnalysisResponse& response) {
|
|
auto action =
|
|
ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED;
|
|
for (auto result : response.results()) {
|
|
auto action2 = GetActionFromResult(result);
|
|
if (action2 > action)
|
|
action = action2;
|
|
}
|
|
return action;
|
|
}
|
|
|
|
void DumpResponse(
|
|
std::stringstream& stream,
|
|
const ContentAnalysisResponse& response) {
|
|
for (auto result : response.results()) {
|
|
auto tag = result.has_tag() ? result.tag() : "<no-tag>";
|
|
|
|
auto status = result.has_status()
|
|
? result.status()
|
|
: ContentAnalysisResponse::Result::STATUS_UNKNOWN;
|
|
std::string status_str;
|
|
switch (status) {
|
|
case ContentAnalysisResponse::Result::STATUS_UNKNOWN:
|
|
status_str = "Unknown";
|
|
break;
|
|
case ContentAnalysisResponse::Result::SUCCESS:
|
|
status_str = "Success";
|
|
break;
|
|
case ContentAnalysisResponse::Result::FAILURE:
|
|
status_str = "Failure";
|
|
break;
|
|
default:
|
|
status_str = "<Uknown>";
|
|
break;
|
|
}
|
|
|
|
auto action = GetActionFromResult(result);
|
|
std::string action_str;
|
|
switch (action) {
|
|
case ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED:
|
|
action_str = "allowed";
|
|
break;
|
|
case ContentAnalysisResponse::Result::TriggeredRule::REPORT_ONLY:
|
|
action_str = "reported only";
|
|
break;
|
|
case ContentAnalysisResponse::Result::TriggeredRule::WARN:
|
|
action_str = "warned";
|
|
break;
|
|
case ContentAnalysisResponse::Result::TriggeredRule::BLOCK:
|
|
action_str = "blocked";
|
|
break;
|
|
}
|
|
|
|
time_t now = time(nullptr);
|
|
stream << "[Demo] Request " << response.request_token() << " is " << action_str
|
|
<< " after " << tag
|
|
<< " analysis, status=" << status_str
|
|
<< " at " << ctime(&now);
|
|
}
|
|
}
|
|
|
|
ContentAnalysisAcknowledgement BuildAcknowledgement(
|
|
const std::string& request_token,
|
|
ContentAnalysisAcknowledgement::FinalAction final_action) {
|
|
ContentAnalysisAcknowledgement ack;
|
|
ack.set_request_token(request_token);
|
|
ack.set_status(ContentAnalysisAcknowledgement::SUCCESS);
|
|
ack.set_final_action(final_action);
|
|
return ack;
|
|
}
|
|
|
|
void HandleRequest(const ContentAnalysisRequest& request) {
|
|
AtomicCout aout;
|
|
ContentAnalysisResponse response;
|
|
int err = client->Send(request, &response);
|
|
if (err != 0) {
|
|
aout.stream() << "[Demo] Error sending request " << request.request_token()
|
|
<< std::endl;
|
|
} else if (response.results_size() == 0) {
|
|
aout.stream() << "[Demo] Response " << request.request_token() << " is missing a result"
|
|
<< std::endl;
|
|
} else {
|
|
DumpResponse(aout.stream(), response);
|
|
|
|
auto final_action = ContentAnalysisAcknowledgement::ALLOW;
|
|
switch (GetActionFromResponse(response)) {
|
|
case ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED:
|
|
break;
|
|
case ContentAnalysisResponse::Result::TriggeredRule::REPORT_ONLY:
|
|
final_action = ContentAnalysisAcknowledgement::REPORT_ONLY;
|
|
break;
|
|
case ContentAnalysisResponse::Result::TriggeredRule::WARN:
|
|
final_action = ContentAnalysisAcknowledgement::WARN;
|
|
break;
|
|
case ContentAnalysisResponse::Result::TriggeredRule::BLOCK:
|
|
final_action = ContentAnalysisAcknowledgement::BLOCK;
|
|
break;
|
|
}
|
|
|
|
// If grouping, remember the request's token in order to ack the response
|
|
// later.
|
|
if (group) {
|
|
std::unique_lock<std::mutex> lock(global_mutex);
|
|
request_tokens.push_back(request.request_token());
|
|
if (final_action > global_final_action)
|
|
global_final_action = final_action;
|
|
} else {
|
|
int err = client->Acknowledge(
|
|
BuildAcknowledgement(response.request_token(), final_action));
|
|
if (err != 0) {
|
|
aout.stream() << "[Demo] Error sending ack " << request.request_token()
|
|
<< std::endl;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProcessRequest(size_t i) {
|
|
auto request = BuildRequest(datas[i]);
|
|
|
|
{
|
|
AtomicCout aout;
|
|
aout.stream() << "[Demo] Sending request " << request.request_token() << std::endl;
|
|
}
|
|
|
|
HandleRequest(request);
|
|
}
|
|
|
|
int main(int argc, char* argv[]) {
|
|
if (!ParseCommandLine(argc, argv)) {
|
|
PrintHelp();
|
|
return 1;
|
|
}
|
|
|
|
// Each client uses a unique name to identify itself with Google Chrome.
|
|
client = Client::Create({path, user_specific});
|
|
if (!client) {
|
|
std::cout << "[Demo] Error starting client" << std::endl;
|
|
return 1;
|
|
};
|
|
|
|
auto info = client->GetAgentInfo();
|
|
std::cout << "Agent pid=" << info.pid
|
|
<< " path=" << info.binary_path << std::endl;
|
|
|
|
if (threaded) {
|
|
std::vector<std::unique_ptr<std::thread>> threads;
|
|
for (int i = 0; i < datas.size(); ++i) {
|
|
AtomicCout aout;
|
|
aout.stream() << "Start thread " << i << std::endl;
|
|
threads.emplace_back(std::make_unique<std::thread>(ProcessRequest, i));
|
|
}
|
|
|
|
// Make sure all threads have terminated.
|
|
for (auto& thread : threads) {
|
|
thread->join();
|
|
}
|
|
}
|
|
else {
|
|
for (size_t i = 0; i < datas.size(); ++i) {
|
|
ProcessRequest(i);
|
|
}
|
|
}
|
|
// It's safe to access global state beyond this point without locking since
|
|
// all no more responses will be touching them.
|
|
|
|
if (group) {
|
|
std::cout << std::endl;
|
|
std::cout << "[Demo] Final action for all requests is ";
|
|
switch (global_final_action) {
|
|
// Google Chrome fails open, so if no action is specified that is the same
|
|
// as ALLOW.
|
|
case ContentAnalysisAcknowledgement::ACTION_UNSPECIFIED:
|
|
case ContentAnalysisAcknowledgement::ALLOW:
|
|
std::cout << "allowed";
|
|
break;
|
|
case ContentAnalysisAcknowledgement::REPORT_ONLY:
|
|
std::cout << "reported only";
|
|
break;
|
|
case ContentAnalysisAcknowledgement::WARN:
|
|
std::cout << "warned";
|
|
break;
|
|
case ContentAnalysisAcknowledgement::BLOCK:
|
|
std::cout << "blocked";
|
|
break;
|
|
}
|
|
std::cout << std::endl << std::endl;
|
|
|
|
for (auto token : request_tokens) {
|
|
std::cout << "[Demo] Sending group Ack" << std::endl;
|
|
int err = client->Acknowledge(
|
|
BuildAcknowledgement(token, global_final_action));
|
|
if (err != 0) {
|
|
std::cout << "[Demo] Error sending ack for " << token << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
};
|