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