1 #include "root_path.hpp"
2 #include "../../common/content_format.hpp"
3 
4 #include <pxp-agent/modules/task.hpp>
5 #include <pxp-agent/configuration.hpp>
6 #include <pxp-agent/util/process.hpp>
7 
8 #include <cpp-pcp-client/protocol/chunks.hpp>       // ParsedChunks
9 
10 #include <leatherman/json_container/json_container.hpp>
11 #include <leatherman/util/scope_exit.hpp>
12 #include <leatherman/file_util/file.hpp>
13 
14 #include <boost/filesystem.hpp>
15 #include <boost/algorithm/string/trim.hpp>
16 #include <boost/algorithm/string/predicate.hpp>
17 #include <boost/algorithm/string/erase.hpp>
18 #include <boost/date_time/posix_time/posix_time_types.hpp>
19 
20 #include <catch.hpp>
21 
22 #include <string>
23 #include <vector>
24 #include <unistd.h>
25 
26 #ifdef _WIN32
27 #define EXTENSION ".bat"
28 #else
29 #define EXTENSION ""
30 #endif
31 
32 using namespace PXPAgent;
33 
34 namespace fs = boost::filesystem;
35 namespace pt = boost::posix_time;
36 namespace lth_jc = leatherman::json_container;
37 namespace lth_util = leatherman::util;
38 namespace lth_file = leatherman::file_util;
39 
40 static const std::string SPOOL_DIR { std::string { PXP_AGENT_ROOT_PATH }
41                                      + "/lib/tests/resources/test_spool" };
42 
43 static const auto STORAGE = std::make_shared<ResultsStorage>(SPOOL_DIR, "0d");
44 
45 static const std::string TASK_CACHE_DIR { std::string { PXP_AGENT_ROOT_PATH }
46                                           + "/lib/tests/resources/tasks-cache" };
47 
48 // Disable cache ttl so we don't delete fixtures.
49 static const std::string TASK_CACHE_TTL { "0d" };
50 
51 static const auto MODULE_CACHE_DIR = std::make_shared<ModuleCacheDir>(TASK_CACHE_DIR, TASK_CACHE_TTL);
52 
53 static const std::string TEMP_TASK_CACHE_DIR { TASK_CACHE_DIR + "/temp" };
54 
55 static const std::vector<std::string> MASTER_URIS { { "https://_master1", "https://_master2", "https://_master3" } };
56 
57 static const std::string CA { "mock_ca" };
58 
59 static const std::string CRT { "mock_crt" };
60 
61 static const std::string KEY { "mock_key" };
62 
63 static const std::string CRL { "mock_crl" };
64 
65 static const std::string NON_BLOCKING_ECHO_TXT {
66     (NON_BLOCKING_DATA_FORMAT % "\"1988\""
67                               % "\"task\""
68                               % "\"run\""
69                               % "{\"task\":\"foo\",\"input\":{\"message\":\"hello\"},"
70                                  "\"files\":[{\"uri\":{\"path\":\"/init" EXTENSION "\","
71                                                       "\"params\":{\"environment\":\"production\"}},"
72                                                       "\"sha256\":\"15f26bdeea9186293d256db95fed616a7b823de947f4e9bd0d8d23c5ac786d13\","
73                                                       "\"filename\":\"init\","
74                                                       "\"size_bytes\":131}]}"
75                               % "false").str() };
76 
77 static const PCPClient::ParsedChunks NON_BLOCKING_CONTENT {
78                     lth_jc::JsonContainer(ENVELOPE_TXT),           // envelope
79                     lth_jc::JsonContainer(NON_BLOCKING_ECHO_TXT),  // data
80                     {},   // debug
81                     0 };  // num invalid debug chunks
82 
83 TEST_CASE("Modules::Task", "[modules]") {
84     SECTION("can successfully instantiate") {
85         REQUIRE_NOTHROW(Modules::Task(PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, MODULE_CACHE_DIR, STORAGE));
86     }
87 }
88 
89 TEST_CASE("Modules::Task::features", "[modules]") {
90     Modules::Task mod { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, MODULE_CACHE_DIR, STORAGE };
91 
92     SECTION("reports available features") {
93         REQUIRE(mod.features().size() > 1);
94         REQUIRE(mod.features().find("puppet-agent") != mod.features().end());
95 #ifdef _WIN32
96         REQUIRE(mod.features().find("powershell") != mod.features().end());
97 #else
98         REQUIRE(mod.features().find("shell") != mod.features().end());
99 #endif
100     }
101 }
102 
103 TEST_CASE("Modules::Task::hasAction", "[modules]") {
104     Modules::Task mod { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, MODULE_CACHE_DIR, STORAGE };
105 
106     SECTION("correctly reports false") {
107         REQUIRE(!mod.hasAction("foo"));
108     }
109 
110     SECTION("correctly reports true") {
111         REQUIRE(mod.hasAction("run"));
112     }
113 }
114 
configureTest()115 static void configureTest() {
116     if (!fs::exists(SPOOL_DIR) && !fs::create_directories(SPOOL_DIR)) {
117         FAIL("Failed to create the results directory");
118     }
119     if (!fs::exists(TEMP_TASK_CACHE_DIR) && !fs::create_directories(TEMP_TASK_CACHE_DIR)) {
120         FAIL("Failed to create the temporary tasks cache directory");
121     }
122     Configuration::Instance().initialize(
123         [](std::vector<std::string>) {
124             return EXIT_SUCCESS;
125         });
126 }
127 
resetTest()128 static void resetTest() {
129     if (fs::exists(SPOOL_DIR)) {
130         fs::remove_all(SPOOL_DIR);
131     }
132     if (fs::exists(TEMP_TASK_CACHE_DIR)) {
133         fs::remove_all(TEMP_TASK_CACHE_DIR);
134     }
135 }
136 
137 TEST_CASE("Modules::Task::callAction", "[modules]") {
138     configureTest();
139     lth_util::scope_exit config_cleaner { resetTest };
140     static const auto temp_module_cache_dir = std::make_shared<ModuleCacheDir>(TEMP_TASK_CACHE_DIR, TASK_CACHE_TTL);
141     Modules::Task e_m { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, temp_module_cache_dir, STORAGE };
142 
143     SECTION("throws module processing error when a file system error is thrown") {
144         auto existent_sha = "existent";
145         boost::nowide::ofstream(TEMP_TASK_CACHE_DIR + "/" + existent_sha);
146         auto task_txt = (DATA_FORMAT % "\"0632\""
147                                      % "\"task\""
148                                      % "\"run\""
149                                      % "{\"task\": \"test::existent\", \"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"existent\"}]}").str();
150         PCPClient::ParsedChunks task_content {
151             lth_jc::JsonContainer(ENVELOPE_TXT),
152             lth_jc::JsonContainer(task_txt),
153             {},
154             0 };
155         ActionRequest request { RequestType::Blocking, task_content };
156 
157         auto response = e_m.executeAction(request);
158         REQUIRE_FALSE(response.action_metadata.includes("results"));
159         REQUIRE_FALSE(response.action_metadata.get<bool>("results_are_valid"));
160         REQUIRE(response.action_metadata.includes("execution_error"));
161         REQUIRE(response.action_metadata.get<std::string>("execution_error").find("Failed to create cache dir to download file to"));
162     }
163 }
164 
165 TEST_CASE("Modules::Task::callAction - non blocking", "[modules]") {
166     configureTest();
167     lth_util::scope_exit config_cleaner { resetTest };
168     Modules::Task e_m { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, MODULE_CACHE_DIR, STORAGE };
169 
170     SECTION("the pid is written to file") {
171         ActionRequest request { RequestType::NonBlocking, NON_BLOCKING_CONTENT };
172         fs::path spool_path { SPOOL_DIR };
173         auto results_dir = (spool_path / request.transactionId()).string();
174         fs::create_directories(results_dir);
175         request.setResultsDir(results_dir);
176         auto pid_path = spool_path / request.transactionId() / "pid";
177 
178         REQUIRE_NOTHROW(e_m.executeAction(request));
179         REQUIRE(fs::exists(pid_path));
180 
181         try {
182             auto pid_txt = lth_file::read(pid_path.string());
183             auto pid = std::stoi(pid_txt);
184         } catch (std::exception&) {
185             FAIL("fail to get pid");
186         }
187     }
188 }
189 
190 TEST_CASE("Modules::Task::executeAction", "[modules][output]") {
191     configureTest();
192     lth_util::scope_exit config_cleaner { resetTest };
193     Modules::Task e_m { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, MODULE_CACHE_DIR, STORAGE };
194 
195     SECTION("passes input as json") {
196         auto echo_txt =
197 #ifndef _WIN32
198             (DATA_FORMAT % "\"0632\""
199                          % "\"task\""
200                          % "\"run\""
201                          % "{\"task\": \"test\", \"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"15f26bdeea9186293d256db95fed616a7b823de947f4e9bd0d8d23c5ac786d13\", \"filename\": \"init\"}]}").str();
202 #else
203             (DATA_FORMAT % "\"0632\""
204                          % "\"task\""
205                          % "\"run\""
206                          % "{\"task\": \"test\", \"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"e1c10f8c709f06f4327ac6a07a918e297a039a24a788fabf4e2ebc31d16e8dc3\", \"filename\": \"init.bat\"}]}").str();
207 #endif
208         PCPClient::ParsedChunks echo_content {
209             lth_jc::JsonContainer(ENVELOPE_TXT),
210             lth_jc::JsonContainer(echo_txt),
211             {},
212             0 };
213         ActionRequest request { RequestType::Blocking, echo_content };
214 
215         auto output = e_m.executeAction(request).action_metadata.get<std::string>({"results", "stdout"});
216         boost::trim(output);
217         REQUIRE(output == "{\"message\":\"hello\",\"_task\":\"test\"}");
218     }
219 
220     SECTION("builds installdir") {
221         auto echo_txt =
222 #ifndef _WIN32
223             (DATA_FORMAT % "\"0632\""
224                          % "\"task\""
225                          % "\"run\""
226                          % "{\"task\": \"test\","
227                            "\"input_method\": \"environment\","
228                            "\"input\":{\"message\":\"hello\"},"
229                            "\"metadata\": {\"files\": [\"dir/\"],"
230                                           "\"implementations\": [{\"name\": \"init.sh\", \"requirements\": [\"shell\"], \"files\": [\"file1.txt\"]}]},"
231                            "\"files\" : [{\"sha256\": \"28dd63928f9e3837e11e36f5af35c09068e4d62b355cf169a873a0cdf30f7c95\", \"filename\": \"init.sh\"},"
232                                         "{\"sha256\": \"67ee5478eaadb034ba59944eb977797b49ca6aa8d3574587f36ebcbeeb65f70e\", \"filename\": \"dir/file2.txt\"},"
233                                         "{\"sha256\": \"94f6e58bd04a4513b8301e75f40527cf7610c66d1960b26f6ac2e743e108bdac\", \"filename\": \"dir/sub_dir/file3.txt\"},"
234                                         "{\"sha256\": \"ecdc5536f73bdae8816f0ea40726ef5e9b810d914493075903bb90623d97b1d8\", \"filename\": \"file1.txt\"}]}").str();
235         auto task_path = fs::path(TASK_CACHE_DIR) / "28dd63928f9e3837e11e36f5af35c09068e4d62b355cf169a873a0cdf30f7c95" / "init.sh";
236 #else
237             (DATA_FORMAT % "\"0632\""
238                          % "\"task\""
239                          % "\"run\""
240                          % "{\"task\": \"test\","
241                            "\"input_method\": \"environment\","
242                            "\"input\":{\"message\":\"hello\"},"
243                            "\"metadata\": {\"files\": [\"dir/\"],"
244                                           "\"implementations\": [{\"name\": \"init.bat\", \"requirements\": [], \"files\": [\"file1.txt\"]}]},"
245                            "\"files\" : [{\"sha256\": \"ea7d652bfe797121a7ac8e3654aacf7d50a9a4665e843669b873995b072d820b\", \"filename\": \"init.bat\"},"
246                                         "{\"sha256\": \"67ee5478eaadb034ba59944eb977797b49ca6aa8d3574587f36ebcbeeb65f70e\", \"filename\": \"dir/file2.txt\"},"
247                                         "{\"sha256\": \"94f6e58bd04a4513b8301e75f40527cf7610c66d1960b26f6ac2e743e108bdac\", \"filename\": \"dir/sub_dir/file3.txt\"},"
248                                         "{\"sha256\": \"ecdc5536f73bdae8816f0ea40726ef5e9b810d914493075903bb90623d97b1d8\", \"filename\": \"file1.txt\"}]}").str();
249         auto task_path = fs::path(TASK_CACHE_DIR) / "ea7d652bfe797121a7ac8e3654aacf7d50a9a4665e843669b873995b072d820b" / "init.bat";
250 #endif
251         std::ifstream task_file { task_path.string() };
252         std::string task_content { std::istreambuf_iterator<char>{task_file}, {} };
253         PCPClient::ParsedChunks echo_content {
254             lth_jc::JsonContainer(ENVELOPE_TXT),
255             lth_jc::JsonContainer(echo_txt),
256             {},
257             0 };
258         ActionRequest request { RequestType::Blocking, echo_content };
259 
260         auto output = e_m.executeAction(request).action_metadata.get<std::string>({"results", "stdout"});
261         boost::trim(output);
262         boost::trim(task_content);
263         REQUIRE(output == "file1\nfile2\nfile3\n"+task_content);
264     }
265 
266     SECTION("passes input only on stdin when input_method is stdin") {
267         auto echo_txt =
268 #ifndef _WIN32
269             (DATA_FORMAT % "\"0632\""
270                          % "\"task\""
271                          % "\"run\""
272                          % "{\"task\": \"test::multi\", \"input\":{\"message\":\"hello\"}, \"input_method\": \"stdin\", \"files\" : [{\"sha256\": \"823c013467ce03b12dbe005757a6c842894373e8bcfb0cf879329afb5abcd543\", \"filename\": \"multi\"}]}").str();
273 #else
274             (DATA_FORMAT % "\"0632\""
275                          % "\"task\""
276                          % "\"run\""
277                          % "{\"task\": \"test::multi\", \"input\":{\"message\":\"hello\"}, \"input_method\": \"stdin\", \"files\" : [{\"sha256\": \"88a07e5b672aa44a91aa7d63e22c91510af5d4707e12f75e0d5de2dfdbde1dec\", \"filename\": \"multi.bat\"}]}").str();
278 #endif
279         PCPClient::ParsedChunks echo_content {
280             lth_jc::JsonContainer(ENVELOPE_TXT),
281             lth_jc::JsonContainer(echo_txt),
282             {},
283             0 };
284         ActionRequest request { RequestType::Blocking, echo_content };
285 
286         auto output = e_m.executeAction(request).action_metadata.get<std::string>({ "results", "stdout" });
287         boost::trim(output);
288 #ifdef _WIN32
289         REQUIRE(output == "ECHO is on.\r\n{\"message\":\"hello\",\"_task\":\"test::multi\"}");
290 #else
291         REQUIRE(output == "{\"message\":\"hello\",\"_task\":\"test::multi\"}");
292 #endif
293     }
294 
295     SECTION("passes input on both stdin and env when method is both") {
296         auto echo_txt =
297 #ifndef _WIN32
298             (DATA_FORMAT % "\"0632\""
299                          % "\"task\""
300                          % "\"run\""
301                          % "{\"task\":\"test::both\", \"input\":{\"message\":\"hello\"}, \"input_method\": \"both\", \"files\" : [{\"sha256\": \"823c013467ce03b12dbe005757a6c842894373e8bcfb0cf879329afb5abcd543\", \"filename\": \"multi\"}]}").str();
302 #else
303             (DATA_FORMAT % "\"0632\""
304                          % "\"task\""
305                          % "\"run\""
306                          % "{\"task\":\"test::both\", \"input\":{\"message\":\"hello\"}, \"input_method\": \"both\", \"files\" : [{\"sha256\": \"88a07e5b672aa44a91aa7d63e22c91510af5d4707e12f75e0d5de2dfdbde1dec\", \"filename\": \"multi.bat\"}]}").str();
307 #endif
308         PCPClient::ParsedChunks echo_content {
309             lth_jc::JsonContainer(ENVELOPE_TXT),
310             lth_jc::JsonContainer(echo_txt),
311             {},
312             0 };
313         ActionRequest request { RequestType::Blocking, echo_content };
314 
315         auto output = e_m.executeAction(request).action_metadata.get<std::string>({ "results", "stdout" });
316         boost::trim(output);
317 #ifdef _WIN32
318         REQUIRE(output == "hello\r\n{\"message\":\"hello\",\"_task\":\"test::both\"}");
319 #else
320         REQUIRE(output == "hello\n{\"message\":\"hello\",\"_task\":\"test::both\"}");
321 #endif
322     }
323 
324     SECTION("passes input as env variables") {
325         auto echo_txt =
326 #ifndef _WIN32
327             (DATA_FORMAT % "\"0632\""
328                          % "\"task\""
329                          % "\"run\""
330                          % "{\"task\":\"test::printer\", \"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"936e85a9b7f1e7b4b593c9f051a36105ed36f7fb8dcff67ff23a3a9af2abe962\", \"filename\": \"printer\"}]}").str();
331 #else
332             (DATA_FORMAT % "\"0632\""
333                          % "\"task\""
334                          % "\"run\""
335                          % "{\"task\":\"test::printer\", \"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"1c616ed98f54880444d0c49036cdf930120457c20e7a9a204db750f2d6162999\", \"filename\": \"printer.bat\"}]}").str();
336 #endif
337         PCPClient::ParsedChunks echo_content {
338             lth_jc::JsonContainer(ENVELOPE_TXT),
339             lth_jc::JsonContainer(echo_txt),
340             {},
341             0 };
342         ActionRequest request { RequestType::Blocking, echo_content };
343 
344         auto output = e_m.executeAction(request).action_metadata.get<std::string>({"results", "stdout"});
345         boost::trim(output);
346         REQUIRE(output == "hello");
347     }
348 
349     SECTION("passes input only as env variables when input_method is environment") {
350         auto echo_txt =
351 #ifndef _WIN32
352             (DATA_FORMAT % "\"0632\""
353                          % "\"task\""
354                          % "\"run\""
355                          % "{\"task\":\"test::multi\", \"input\":{\"message\":\"hello\"}, \"input_method\": \"environment\", \"files\" : [{\"sha256\": \"823c013467ce03b12dbe005757a6c842894373e8bcfb0cf879329afb5abcd543\", \"filename\": \"multi\"}]}").str();
356 #else
357             (DATA_FORMAT % "\"0632\""
358                          % "\"task\""
359                          % "\"run\""
360                          % "{\"task\":\"test::multi\", \"input\":{\"message\":\"hello\"}, \"input_method\": \"environment\", \"files\" : [{\"sha256\": \"88a07e5b672aa44a91aa7d63e22c91510af5d4707e12f75e0d5de2dfdbde1dec\", \"filename\": \"multi.bat\"}]}").str();
361 #endif
362         PCPClient::ParsedChunks echo_content {
363             lth_jc::JsonContainer(ENVELOPE_TXT),
364             lth_jc::JsonContainer(echo_txt),
365             {},
366             0 };
367         ActionRequest request { RequestType::Blocking, echo_content };
368 
369         auto output = e_m.executeAction(request).action_metadata.get<std::string>({ "results", "stdout" });
370         boost::trim(output);
371         REQUIRE(output == "hello");
372     }
373 
374     SECTION("succeeds on non-zero exit") {
375         auto echo_txt =
376 #ifndef _WIN32
377             (DATA_FORMAT % "\"0632\""
378                          % "\"task\""
379                          % "\"run\""
380                          % "{\"task\": \"test::error\", \"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\" : \"d5b8819b51ecd53b32de74c09def0e71f617076bc8e4f75e1eac99b8f77a6c70\", \"filename\": \"error\"}]}").str();
381 #else
382             (DATA_FORMAT % "\"0632\""
383                          % "\"task\""
384                          % "\"run\""
385                          % "{\"task\": \"test::error\", \"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\" : \"554f86a33add88c371c2bbb79839c9adfd3d420dc5f405a07e97fab54efbe1ba\", \"filename\": \"error.bat\"}]}").str();
386 #endif
387         PCPClient::ParsedChunks echo_content {
388             lth_jc::JsonContainer(ENVELOPE_TXT),
389             lth_jc::JsonContainer(echo_txt),
390             {},
391             0 };
392         ActionRequest request { RequestType::Blocking, echo_content };
393         auto response = e_m.executeAction(request);
394 
395         auto output = response.action_metadata.get<std::string>({"results", "stdout"});
396         boost::trim(output);
397         REQUIRE(output == "hello");
398         REQUIRE(response.action_metadata.get<bool>("results_are_valid"));
399         boost::trim(response.output.std_out);
400         REQUIRE(response.output.std_out == "hello");
401         boost::trim(response.output.std_err);
402         REQUIRE(response.output.std_err == "goodbye");
403         REQUIRE(response.output.exitcode == 1);
404     }
405 
406     SECTION("errors on unparseable output") {
407         auto echo_txt =
408 #ifndef _WIN32
409             (DATA_FORMAT % "\"0632\""
410                          % "\"task\""
411                          % "\"run\""
412                          % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, "
413                            "\"files\" : [{\"sha256\": \"d2795e0a1b66ca75be9e2be25c2a61fdbab9efc641f8e480f5ab1b348112701d\", \"filename\": \"unparseable\"}]}").str();
414 #else
415             (DATA_FORMAT % "\"0632\""
416                          % "\"task\""
417                          % "\"run\""
418                          % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, "
419                            "\"files\" : [{\"sha256\": \"0d75633b5dd4b153496b4593e9d94e69265d2a812579f724ba0b4422b0bfb836\", \"filename\": \"unparseable.bat\"}]}").str();
420 #endif
421         PCPClient::ParsedChunks echo_content {
422             lth_jc::JsonContainer(ENVELOPE_TXT),
423             lth_jc::JsonContainer(echo_txt),
424             {},
425             0 };
426         ActionRequest request { RequestType::Blocking, echo_content };
427         auto response = e_m.executeAction(request);
428 
429         REQUIRE_FALSE(response.action_metadata.includes("results"));
430         REQUIRE_FALSE(response.action_metadata.get<bool>("results_are_valid"));
431         REQUIRE(response.action_metadata.includes("execution_error"));
432         REQUIRE(response.action_metadata.get<std::string>("execution_error") == "The task executed for the blocking 'task run' request (transaction 0632) returned invalid UTF-8 on stdout");
433         boost::trim(response.output.std_out);
434         unsigned char badchar[3] = {0xed, 0xbf, 0xbf};
435         REQUIRE(response.output.std_out == std::string(reinterpret_cast<char*>(badchar), 3));
436         REQUIRE(response.output.std_err == "");
437         REQUIRE(response.output.exitcode == 0);
438     }
439 
440 // Unclear how to create a script on Windows that can print a null byte...
441 #ifndef _WIN32
442     SECTION("errors on output with null bytes") {
443         auto echo_txt =
444             (DATA_FORMAT % "\"0632\""
445                          % "\"task\""
446                          % "\"run\""
447                          % "{\"task\": \"null_byte\", \"input\":{\"message\":\"hello\"}, "
448                            "\"files\" : [{\"sha256\": \"b26e34bc50c88ca5ee2bfcbcaff5c23b0124db9479e66390539f2715b675b7e7\", \"filename\": \"null_byte\"}]}").str();
449         PCPClient::ParsedChunks echo_content {
450             lth_jc::JsonContainer(ENVELOPE_TXT),
451             lth_jc::JsonContainer(echo_txt),
452             {},
453             0 };
454         ActionRequest request { RequestType::Blocking, echo_content };
455         auto response = e_m.executeAction(request);
456 
457         REQUIRE_FALSE(response.action_metadata.includes("results"));
458         REQUIRE_FALSE(response.action_metadata.get<bool>("results_are_valid"));
459         REQUIRE(response.action_metadata.includes("execution_error"));
460         REQUIRE(response.action_metadata.get<std::string>("execution_error") == "The task executed for the blocking 'task run' request (transaction 0632) returned invalid UTF-8 on stdout");
461         boost::trim(response.output.std_out);
462         unsigned char badchar[7] = {'f', 'o', 'o', 0x00, 'b', 'a', 'r'};
463         REQUIRE(response.output.std_out == std::string(reinterpret_cast<char*>(badchar), 7));
464         REQUIRE(response.output.std_err == "");
465         REQUIRE(response.output.exitcode == 0);
466     }
467 #endif
468 
469     SECTION("errors on download when no master-uri is provided") {
470         Modules::Task e_m { PXP_AGENT_BIN_PATH, {}, CA, CRT, KEY, CRL, "", 10, 20, MODULE_CACHE_DIR, STORAGE };
471         auto task_txt = (DATA_FORMAT % "\"0632\""
472                                      % "\"task\""
473                                      % "\"run\""
474                                      % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, "
475                                        "\"files\" : [{\"sha256\": \"some_sha\", \"filename\": \"some_file\"}]}").str();
476         PCPClient::ParsedChunks task_content {
477             lth_jc::JsonContainer(ENVELOPE_TXT),
478             lth_jc::JsonContainer(task_txt),
479             {},
480             0 };
481         ActionRequest request { RequestType::Blocking, task_content };
482         auto response = e_m.executeAction(request);
483 
484         REQUIRE_FALSE(response.action_metadata.includes("results"));
485         REQUIRE_FALSE(response.action_metadata.get<bool>("results_are_valid"));
486         REQUIRE(response.action_metadata.includes("execution_error"));
487         REQUIRE(boost::contains(response.action_metadata.get<std::string>("execution_error"), "Cannot download file. No master-uris were provided"));
488     }
489 
490     SECTION("if a master-uri has a server-side error, then it proceeds to try the next master-uri. if they all fail, it errors on download and removes all temporary files") {
491         static const auto temp_module_cache_dir = std::make_shared<ModuleCacheDir>(TEMP_TASK_CACHE_DIR, TASK_CACHE_TTL);
492         Modules::Task e_m { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, temp_module_cache_dir, STORAGE };
493         auto cache = fs::path(TEMP_TASK_CACHE_DIR) / "some_sha";
494         auto task_txt = (DATA_FORMAT % "\"0632\""
495                                      % "\"task\""
496                                      % "\"run\""
497                                      % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, "
498                                        "\"files\" : [{\"uri\": { \"path\": \"bad_path\", \"params\": { \"environment\": \"production\", \"code-id\": \"1234\" }}, \"sha256\": \"some_sha\", \"filename\": \"some_file\"}]}").str();
499         PCPClient::ParsedChunks task_content {
500             lth_jc::JsonContainer(ENVELOPE_TXT),
501             lth_jc::JsonContainer(task_txt),
502             {},
503             0 };
504         ActionRequest request { RequestType::Blocking, task_content };
505         auto response = e_m.executeAction(request);
506 
507         REQUIRE_FALSE(response.action_metadata.includes("results"));
508         REQUIRE_FALSE(response.action_metadata.get<bool>("results_are_valid"));
509         REQUIRE(response.action_metadata.includes("execution_error"));
510         std::string expected_master_uri = MASTER_URIS.back();
511         boost::erase_head(expected_master_uri, std::string("https://").length());
512         REQUIRE_THAT(response.action_metadata.get<std::string>("execution_error"), Catch::Contains(expected_master_uri));
513         // Make sure temp file is removed.
514         REQUIRE(fs::is_empty(cache));
515     }
516 
517     SECTION("creates the tasks-cache/<sha> directory with ower/group read and write permissions") {
518         static const auto temp_module_cache_dir = std::make_shared<ModuleCacheDir>(TEMP_TASK_CACHE_DIR, TASK_CACHE_TTL);
519         Modules::Task e_m { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, temp_module_cache_dir, STORAGE };
520         auto cache = fs::path(TEMP_TASK_CACHE_DIR) / "some_other_sha";
521         auto task_txt = (DATA_FORMAT % "\"0632\""
522                                      % "\"task\""
523                                      % "\"run\""
524                                      % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, "
525                                        "\"files\" : [{\"sha256\": \"some_other_sha\",  \"filename\": \"some_file\"}]}").str();
526         PCPClient::ParsedChunks task_content {
527             lth_jc::JsonContainer(ENVELOPE_TXT),
528             lth_jc::JsonContainer(task_txt),
529             {},
530             0 };
531         ActionRequest request { RequestType::Blocking, task_content };
532         e_m.executeAction(request);
533 
534         REQUIRE(fs::exists(cache));
535         REQUIRE(fs::is_directory(cache));
536 #ifndef _WIN32
537         REQUIRE(fs::status(cache).permissions() == 0750);
538 #else
539         REQUIRE(fs::status(cache).permissions() == 0666);
540 #endif
541     }
542 
543     SECTION("succeeds even if tasks-cache was deleted") {
544         static const auto temp_module_cache_dir = std::make_shared<ModuleCacheDir>(TEMP_TASK_CACHE_DIR, TASK_CACHE_TTL);
545         Modules::Task e_m { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, temp_module_cache_dir, STORAGE };
546         fs::remove_all(TEMP_TASK_CACHE_DIR);
547 
548         auto cache = fs::path(TEMP_TASK_CACHE_DIR) / "some_other_sha";
549         auto task_txt = (DATA_FORMAT % "\"0632\""
550                                      % "\"task\""
551                                      % "\"run\""
552                                      % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, "
553                                        "\"files\" : [{\"sha256\": \"some_other_sha\"}]}").str();
554         PCPClient::ParsedChunks task_content {
555             lth_jc::JsonContainer(ENVELOPE_TXT),
556             lth_jc::JsonContainer(task_txt),
557             {},
558             0 };
559         ActionRequest request { RequestType::Blocking, task_content };
560         e_m.executeAction(request);
561 
562         REQUIRE(fs::exists(cache));
563         REQUIRE(fs::is_directory(cache));
564     }
565 }
566 
567 template <typename T>
getEchoRequest(T & metadata)568 static ActionRequest getEchoRequest(T& metadata)
569 {
570     auto files = "["
571         "{\"sha256\": \"823c013467ce03b12dbe005757a6c842894373e8bcfb0cf879329afb5abcd543\", \"filename\": \"multi\"},"
572         "{\"sha256\": \"88a07e5b672aa44a91aa7d63e22c91510af5d4707e12f75e0d5de2dfdbde1dec\", \"filename\": \"multi.bat\"}"
573         "]";
574     auto params = boost::format("{\"task\": \"test::multi\", \"metadata\": %1%, \"input\":{\"message\":\"hello\"}, \"files\": %2%}") % metadata % files;
575     auto echo_txt = (DATA_FORMAT % "\"0632\"" % "\"task\"" % "\"run\"" % params).str();
576     PCPClient::ParsedChunks echo_content {
577         lth_jc::JsonContainer(ENVELOPE_TXT),
578         lth_jc::JsonContainer(echo_txt),
579         {},
580         0 };
581     return ActionRequest { RequestType::Blocking, echo_content };
582 }
583 
584 TEST_CASE("Modules::Task::executeAction implementations", "[modules][output]") {
585     configureTest();
586     lth_util::scope_exit config_cleaner { resetTest };
587     Modules::Task e_m { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, MODULE_CACHE_DIR, STORAGE };
588 
589     SECTION("uses a matching implementation") {
590         auto impls = "["
591             "{\"name\": \"multi\", \"requirements\": [\"shell\"]},"
592             "{\"name\": \"multi.bat\", \"requirements\": [\"powershell\"]},"
593             "{\"name\": \"invalid\"}"
594             "]";
595         auto metadata = boost::format("{\"implementations\": %1%, \"input_method\": \"environment\", \"features\": [\"foobar\"]}") % impls;
596 
597         auto output = e_m.executeAction(getEchoRequest(metadata)).action_metadata.get<std::string>({ "results", "stdout" });
598         boost::trim(output);
599         REQUIRE(output == "hello");
600     }
601 
602     SECTION("uses an unrestricted implementation") {
603         auto impls = "["
604             "{\"name\": \"invalid\", \"requirements\": [\"foobar\"]},"
605 #ifdef _WIN32
606             "{\"name\": \"multi.bat\"}"
607 #else
608             "{\"name\": \"multi\"}"
609 #endif
610             "]";
611         auto metadata = boost::format("{\"implementations\": %1%, \"input_method\": \"environment\"}") % impls;
612 
613         auto output = e_m.executeAction(getEchoRequest(metadata)).action_metadata.get<std::string>({ "results", "stdout" });
614         boost::trim(output);
615         REQUIRE(output == "hello");
616     }
617 
618     SECTION("accepts features as an argument") {
619         auto impls = "["
620 #ifdef _WIN32
621             "{\"name\": \"multi.bat\", \"requirements\": [\"foobar\"]}"
622 #else
623             "{\"name\": \"multi\", \"requirements\": [\"foobar\"]}"
624 #endif
625             "]";
626         auto metadata = boost::format("{\"implementations\": %1%, \"input_method\": \"environment\", \"features\": [\"foobar\"]}") % impls;
627 
628         auto output = e_m.executeAction(getEchoRequest(metadata)).action_metadata.get<std::string>({ "results", "stdout" });
629         boost::trim(output);
630         REQUIRE(output == "hello");
631     }
632 
633     SECTION("uses the implementation's input_method") {
634         auto impls = "["
635 #ifdef _WIN32
636             "{\"name\": \"multi.bat\", \"input_method\": \"environment\"}"
637 #else
638             "{\"name\": \"multi\", \"input_method\": \"environment\"}"
639 #endif
640             "]";
641         auto metadata = boost::format("{\"implementations\": %1%}") % impls;
642 
643         auto output = e_m.executeAction(getEchoRequest(metadata)).action_metadata.get<std::string>({ "results", "stdout" });
644         boost::trim(output);
645         REQUIRE(output == "hello");
646     }
647 
648     SECTION("errors if no implementations are accepted") {
649         auto impls = "[{\"name\": \"invalid\", \"requirements\": [\"foobar\"]}]";
650         auto metadata = boost::format("{\"implementations\": %1%, \"input_method\": \"environment\"}") % impls;
651 
652         auto response = e_m.executeAction(getEchoRequest(metadata));
653         REQUIRE_FALSE(response.action_metadata.includes("results"));
654         REQUIRE_FALSE(response.action_metadata.get<bool>("results_are_valid"));
655         REQUIRE(response.action_metadata.includes("execution_error"));
656         REQUIRE(boost::contains(response.action_metadata.get<std::string>("execution_error"),
657                                 "no implementations match supported features: "));
658     }
659 
660     SECTION("errors if implementation specifies file that doesn't exist") {
661         auto impls = "[{\"name\": \"invalid\"}]";
662         auto metadata = boost::format("{\"implementations\": %1%, \"input_method\": \"environment\"}") % impls;
663 
664         auto response = e_m.executeAction(getEchoRequest(metadata));
665         REQUIRE_FALSE(response.action_metadata.includes("results"));
666         REQUIRE_FALSE(response.action_metadata.get<bool>("results_are_valid"));
667         REQUIRE(response.action_metadata.includes("execution_error"));
668         REQUIRE(boost::contains(response.action_metadata.get<std::string>("execution_error"),
669                                 "'invalid' file requested by implementation not found"));
670     }
671 }
672 
673 // Present in Boost 1.58. That's currently not required, so reproducing it here since it's simple.
my_to_time_t(pt::ptime t)674 static std::time_t my_to_time_t(pt::ptime t)
675 {
676     return (t - pt::ptime(boost::gregorian::date(1970, 1, 1))).total_seconds();
677 }
678 
679 TEST_CASE("purge old tasks", "[modules]") {
680     const std::string PURGE_TASK_CACHE { std::string { PXP_AGENT_ROOT_PATH }
681         + "/lib/tests/resources/purge_test" };
682 
683     const std::string OLD_TRANSACTION { "valid_old" };
684     const std::string RECENT_TRANSACTION { "valid_recent" };
685 
686     static const auto PURGE_MODULE_CACHE_DIR = std::make_shared<ModuleCacheDir>(PURGE_TASK_CACHE, TASK_CACHE_TTL);
687 
688     // Start with 0 TTL to prevent initial cleanup
689     Modules::Task e_m { PXP_AGENT_BIN_PATH, MASTER_URIS, CA, CRT, KEY, CRL, "", 10, 20, PURGE_MODULE_CACHE_DIR, STORAGE };
690 
691     unsigned int num_purged_results { 0 };
692     auto purgeCallback =
__anon6069c83b0202(const std::string&) 693         [&num_purged_results](const std::string&) -> void { num_purged_results++; };
694 
695     SECTION("Purges only the old results based on ttl") {
696         num_purged_results = 0;
697         auto now = pt::second_clock::universal_time();
698         auto recent = now - pt::minutes(50);
699         auto old = now - pt::minutes(61);
700         fs::last_write_time(fs::path(PURGE_TASK_CACHE)/OLD_TRANSACTION, my_to_time_t(old));
701         fs::last_write_time(fs::path(PURGE_TASK_CACHE)/RECENT_TRANSACTION, my_to_time_t(recent));
702 
703         auto results = e_m.purge("1h", {}, purgeCallback);
704         REQUIRE(results == 1);
705         REQUIRE(num_purged_results == 1);
706     }
707 }
708