From b4d2ab5757f66eb1ce6c3cdebad587aa6c260dfe Mon Sep 17 00:00:00 2001 From: Andrea Terzolo Date: Tue, 23 Jul 2024 18:06:10 +0200 Subject: [PATCH] cleanup: complete the /proc scan Signed-off-by: Andrea Terzolo --- plugins/k8smeta/README.md | 7 +- plugins/k8smeta/falco.yaml | 1 + plugins/k8smeta/src/grpc_client.cpp | 2 +- plugins/k8smeta/src/plugin.cpp | 109 +++++++++++---- plugins/k8smeta/src/plugin.h | 4 +- .../k8smeta/src/shared_with_tests_consts.h | 1 + plugins/k8smeta/test/README.md | 2 +- .../test/include/k8smeta_tests/helpers.h | 2 +- plugins/k8smeta/test/src/check_events.cpp | 12 -- plugins/k8smeta/test/src/init_config.cpp | 12 ++ plugins/k8smeta/test/src/parsing_pod.cpp | 125 +++++++++++++++++- 11 files changed, 234 insertions(+), 43 deletions(-) diff --git a/plugins/k8smeta/README.md b/plugins/k8smeta/README.md index f8c5db0b..581dfbbb 100644 --- a/plugins/k8smeta/README.md +++ b/plugins/k8smeta/README.md @@ -81,7 +81,12 @@ plugins: # path to the PEM encoding of the server root certificates. (optional) # Used to open an authanticated GRPC channel with the collector. # If empty the connection will be insecure. - caPEMBundle: /etc/ssl/certs/ca-certificates.crt + caPEMBundle: /etc/ssl/certs/ca-certificates.crt + # The plugin needs to scan the '/proc' of the host on which is running. (optional) + # In Falco usually we put the host '/proc' folder under '/host/proc' so + # the the default for this config is '/host'. + # The path used here must not have a final '/'. + hostPath: /host # (default: /host) load_plugins: [k8smeta] ``` diff --git a/plugins/k8smeta/falco.yaml b/plugins/k8smeta/falco.yaml index d5a62a0d..1248e81b 100644 --- a/plugins/k8smeta/falco.yaml +++ b/plugins/k8smeta/falco.yaml @@ -11,6 +11,7 @@ plugins: collectorHostname: localhost nodeName: kind-control-plane verbosity: critical + hostPath: /host stdout_output: enabled: true diff --git a/plugins/k8smeta/src/grpc_client.cpp b/plugins/k8smeta/src/grpc_client.cpp index b36b1013..66151ebe 100644 --- a/plugins/k8smeta/src/grpc_client.cpp +++ b/plugins/k8smeta/src/grpc_client.cpp @@ -45,7 +45,7 @@ K8sMetaClient::K8sMetaClient(const std::string& node_name, sel.set_nodename(node_name); sel.clear_resourcekinds(); - /// todo! one day we could expose them to the user. + /// todo!: one day we could expose them to the user. (*sel.mutable_resourcekinds())["Pod"] = "true"; (*sel.mutable_resourcekinds())["Namespace"] = "true"; (*sel.mutable_resourcekinds())["Deployment"] = "true"; diff --git a/plugins/k8smeta/src/plugin.cpp b/plugins/k8smeta/src/plugin.cpp index 12ee120c..13d18db2 100644 --- a/plugins/k8smeta/src/plugin.cpp +++ b/plugins/k8smeta/src/plugin.cpp @@ -63,10 +63,12 @@ std::string get_pod_uid_from_cgroup_string(const std::string& cgroup_first_line) // Example: // `cpuset=/kubelet.slice/kubelet-kubepods.slice/kubelet-kubepods-pod05869489-8c7f-45dc-9abd-1b1620787bb1.slice/cri-containerd-2f92446a3fbfd0b7a73457b45e96c75a25c5e44e7b1bcec165712b906551c261.scope\0` // - // 2 - If it arrives from the /proc scan -> `hierarchy - // ID:controller:cgroup_path` Check if the cgroup version is relevant here - // or not... - // todo!: i'm not sure if all controllers have the same format in cgroupv1 + // 2 - If it arrives from the /proc scan -> + // `hierarchyID:controller:cgroup_path` + // Example (cgroup v2): + // `0::/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod93f64796_43b9_468d_b77b_c652c985d5e0.slice` + // Example (cgroup v1): + // `12:perf_event:/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod93f64796_43b9_468d_b77b_c652c985d5e0.slice` if(re2::RE2::PartialMatch(cgroup_first_line, pattern, &pod_uid)) { // Here `pod_uid` could have 2 possible layouts: @@ -90,7 +92,6 @@ std::string get_pod_uid_from_cgroup_string(const std::string& cgroup_first_line) falcosecurity::init_schema my_plugin::get_init_schema() { - /// todo!: check config names falcosecurity::init_schema init_schema; init_schema.schema_type = falcosecurity::init_schema_type::SS_PLUGIN_SCHEMA_JSON; @@ -134,6 +135,11 @@ falcosecurity::init_schema my_plugin::get_init_schema() "type": "string", "title": "The path to the PEM encoding of the server root certificates", "description": "The path to the PEM encoding of the server root certificates. E.g. '/etc/ssl/certs/ca-certificates.crt'" + }, + "hostProc": { + "type": "string", + "title": "Path to reach the '/proc' folder we want to scan.", + "description": "The plugin needs to scan the '/proc' of the host on which is running. In Falco usually we put the host '/proc' folder under '/host/proc' so the the default for this config is '/host'. The path used here must not have a final '/'." } }, "additionalProperties": false, @@ -189,7 +195,7 @@ void my_plugin::parse_init_config(nlohmann::json& config_json) config_json.at(nlohmann::json::json_pointer(NODENAME_PATH)) .get_to(nodename_string); - // todo!: remove it when we solved in Falco + // todo!: Solved in Falco 0.37.0 wait until Falco 0.36.2 is barely used // This is just a simple workaround until we solve the Falco issue // If the provided string is an env variable we use the content // of the env variable @@ -250,6 +256,17 @@ void my_plugin::parse_init_config(nlohmann::json& config_json) } } } + + if(config_json.contains(nlohmann::json::json_pointer(HOST_PROC_PATH))) + { + config_json.at(nlohmann::json::json_pointer(HOST_PROC_PATH)) + .get_to(m_host_proc); + } + else + { + // Default value + m_host_proc = "/host"; + } } void my_plugin::do_initial_proc_scan() @@ -258,7 +275,7 @@ void my_plugin::do_initial_proc_scan() std::string proc_root = m_host_proc + "/proc"; try { - SPDLOG_DEBUG("Start the /proc scan under: '{}'", proc_root); + SPDLOG_INFO("Start the process scan under: '{}'", proc_root); dir_iter = std::filesystem::directory_iterator(proc_root); } catch(std::filesystem::filesystem_error& err) @@ -290,7 +307,6 @@ void my_plugin::do_initial_proc_scan() .append("/") .append(file_name.c_str()) .append("/cgroup"); - SPDLOG_TRACE("Try to scan under: '{}'", proc_path); std::ifstream file(proc_path); @@ -299,12 +315,13 @@ void my_plugin::do_initial_proc_scan() // Read the first line from the file if(std::getline(file, cgroup_line)) { - // todo!: check the cgroupv1 layout std::string pod_uid = get_pod_uid_from_cgroup_string(cgroup_line); if(!pod_uid.empty()) { m_thread_id_pod_uid_map[tid] = pod_uid; + SPDLOG_TRACE("Found thread with tid '{}' and pod uid '{}'", + tid, pod_uid); } } else @@ -316,9 +333,12 @@ void my_plugin::do_initial_proc_scan() } else { - SPDLOG_WARN("cannot open '{}'", proc_path); + SPDLOG_WARN("cannot open '{}'. Skip it.", proc_path); } } + SPDLOG_INFO( + "Process scan correctly completed. Found '{}' threads inside pods.", + m_thread_id_pod_uid_map.size()); } bool my_plugin::init(falcosecurity::init_input& in) @@ -653,7 +673,7 @@ bool inline my_plugin::extract_name_from_meta( nlohmann::json& meta_json, falcosecurity::extract_request& req) { std::string resource_name; - // todo! Possible optimization here and in some other places, some paths + // todo!: Possible optimization here and in some other places, some paths // should always be there. if(!meta_json.contains(nlohmann::json::json_pointer(NAME_PATH))) { @@ -1042,15 +1062,19 @@ bool my_plugin::extract(const falcosecurity::extract_fields_input& in) // The process is not into a pod, stop here. if(pod_uid.empty()) { - // We try to obtain the pod_uid from our internal cache populated during - // the initial /proc scan + // If we fall here and our cache is empty, it means that probably we are + // not in a pod. + if(m_thread_id_pod_uid_map.empty()) + { + return false; + } + + // If the cache is not empty we try to search the pod_uid in the cache. auto it = m_thread_id_pod_uid_map.find(thread_id); if(it == m_thread_id_pod_uid_map.end()) { return false; } - // The ideal thing would be to write it in the sinsp thread table but in - // the extraction phase we don't have a table writer. pod_uid = it->second; } @@ -1336,7 +1360,7 @@ bool inline my_plugin::parse_process_events( return false; } - /// todo! Possible optimization, we can set the pod_uid only if we are in a + /// todo!: Possible optimization, we can set the pod_uid only if we are in a /// container // but we need to access the `m_flags` field to understand if we are in a // container or not. It's also true that if we enable this plugin we are in @@ -1359,14 +1383,19 @@ bool inline my_plugin::parse_process_events( std::string cgroup_first_charbuf = (char*)cgroup_param.param_pointer; std::string pod_uid = get_pod_uid_from_cgroup_string(cgroup_first_charbuf); - // retrieve thread entry associated with the event tid - auto& tr = in.get_table_reader(); - auto thread_entry = m_thread_table.get_entry( - tr, (int64_t)in.get_event_reader().get_tid()); + // If we don't have a pod_uid we don't need to populate the table + if(pod_uid != "") + { + // retrieve thread entry associated with the event tid + auto& tr = in.get_table_reader(); + auto thread_entry = m_thread_table.get_entry( + tr, (int64_t)in.get_event_reader().get_tid()); - // Write the pod_uid into the entry - auto& tw = in.get_table_writer(); - m_pod_uid_field.write_value(tw, thread_entry, (const char*)pod_uid.c_str()); + // Write the pod_uid into the entry + auto& tw = in.get_table_writer(); + m_pod_uid_field.write_value(tw, thread_entry, + (const char*)pod_uid.c_str()); + } return true; } @@ -1375,6 +1404,40 @@ bool my_plugin::parse_event(const falcosecurity::parse_event_input& in) // NOTE: today in the libs framework, parsing errors are not logged auto& evt = in.get_event_reader(); + // Workaround: the parsing is the unique place where we can populate the + // sinsp thread table. The first time we call parse we populate the sinsp + // table and we clear our internal cache. + if(!m_sinsp_proc_populated) + { + auto& tr = in.get_table_reader(); + auto& tw = in.get_table_writer(); + falcosecurity::table_entry thread_entry; + + SPDLOG_INFO("Update the framework state with the plugin cache. The " + "cache has '{}' " + "elements", + m_thread_id_pod_uid_map.size()); + + for(auto it = m_thread_id_pod_uid_map.begin(); + it != m_thread_id_pod_uid_map.end(); ++it) + { + try + { + thread_entry = m_thread_table.get_entry(tr, (int64_t)it->first); + m_pod_uid_field.write_value(tw, thread_entry, + (const char*)it->second.c_str()); + } + catch(falcosecurity::plugin_exception e) + { + SPDLOG_WARN("Thead id '{}' with pod_uid '{}' is not found " + "inside the framework table. Skip it.", + it->first, it->second); + } + } + m_thread_id_pod_uid_map.clear(); + m_sinsp_proc_populated = true; + } + switch(evt.get_type()) { case PPME_ASYNCEVENT_E: diff --git a/plugins/k8smeta/src/plugin.h b/plugins/k8smeta/src/plugin.h index c72827ba..889de308 100644 --- a/plugins/k8smeta/src/plugin.h +++ b/plugins/k8smeta/src/plugin.h @@ -254,7 +254,6 @@ class my_plugin std::string m_collector_port; std::string m_node_name; std::string m_ca_PEM_encoding; - // todo!: populate it when parsing the config. std::string m_host_proc; // State tables @@ -268,6 +267,9 @@ class my_plugin std::unordered_map m_deamonset_table; std::unordered_map m_thread_id_pod_uid_map; + // The first time we parse an event we populate the sinsp thread table and + // we set it to true + bool m_sinsp_proc_populated = false; // Last error of the plugin std::string m_lasterr; // Accessor to the thread table diff --git a/plugins/k8smeta/src/shared_with_tests_consts.h b/plugins/k8smeta/src/shared_with_tests_consts.h index 507ff80c..347f4619 100644 --- a/plugins/k8smeta/src/shared_with_tests_consts.h +++ b/plugins/k8smeta/src/shared_with_tests_consts.h @@ -99,3 +99,4 @@ limitations under the License. #define PORT_PATH "/collectorPort" #define NODENAME_PATH "/nodeName" #define CA_CERT_PATH "/caPEMBundle" +#define HOST_PROC_PATH "/hostProc" diff --git a/plugins/k8smeta/test/README.md b/plugins/k8smeta/test/README.md index a789752c..85a4d636 100644 --- a/plugins/k8smeta/test/README.md +++ b/plugins/k8smeta/test/README.md @@ -20,5 +20,5 @@ To run only some tests you need to use the test binary directly ```bash # from the `build` directory -sudo ./libs_tests/libsinsp/test/unit-test-libsinsp --gtest_filter='*plugin_k8s_PPME_SYSCALL_CLONE3_X_parse' +./libs_tests/libsinsp/test/unit-test-libsinsp --gtest_filter='*plugin_k8s_PPME_SYSCALL_CLONE3_X_parse' ``` diff --git a/plugins/k8smeta/test/include/k8smeta_tests/helpers.h b/plugins/k8smeta/test/include/k8smeta_tests/helpers.h index 6937d807..d4d86445 100644 --- a/plugins/k8smeta/test/include/k8smeta_tests/helpers.h +++ b/plugins/k8smeta/test/include/k8smeta_tests/helpers.h @@ -21,7 +21,7 @@ limitations under the License. #define INIT_CONFIG \ "{\"collectorHostname\":\"localhost\",\"collectorPort\": " \ "45000,\"nodeName\":\"control-plane\",\"verbosity\":" \ - "\"info\"}" + "\"info\", \"hostProc\":\"\"}" #define ASSERT_STRING_SETS(a, b) \ { \ diff --git a/plugins/k8smeta/test/src/check_events.cpp b/plugins/k8smeta/test/src/check_events.cpp index b154dcc4..725441ff 100644 --- a/plugins/k8smeta/test/src/check_events.cpp +++ b/plugins/k8smeta/test/src/check_events.cpp @@ -674,15 +674,3 @@ TEST_F(sinsp_with_test_input, plugin_k8s_update_a_pod) "10.16.1.20"); m_inspector.close(); } - -//////////////////////////////////// -// Missing tests -////////////////////////////////// - -/// todo! Add some tests - -// add a test on a resource without the `/labels` key. - -// Check on a scap file - -// Read a scap-file/huge json file and evaluate perf diff --git a/plugins/k8smeta/test/src/init_config.cpp b/plugins/k8smeta/test/src/init_config.cpp index b624a5b7..4116d665 100644 --- a/plugins/k8smeta/test/src/init_config.cpp +++ b/plugins/k8smeta/test/src/init_config.cpp @@ -92,3 +92,15 @@ TEST_F(sinsp_with_test_input, plugin_k8s_env_variable) err)); ASSERT_EQ(err, ""); } + +TEST_F(sinsp_with_test_input, plugin_k8s_with_host_proc) +{ + auto plugin_owner = m_inspector.register_plugin(PLUGIN_PATH); + ASSERT_TRUE(plugin_owner.get()); + std::string err; + + ASSERT_NO_THROW(plugin_owner->init(R"( +{"collectorHostname":"localhost","collectorPort":45000,"nodeName":"kind-control-plane", "hostProc": "/host"})", + err)); + ASSERT_EQ(err, ""); +} diff --git a/plugins/k8smeta/test/src/parsing_pod.cpp b/plugins/k8smeta/test/src/parsing_pod.cpp index 2a0f9f8a..ae878104 100644 --- a/plugins/k8smeta/test/src/parsing_pod.cpp +++ b/plugins/k8smeta/test/src/parsing_pod.cpp @@ -18,6 +18,8 @@ limitations under the License. #include #include #include +#include +#include // Obtained from the plugin folder #include @@ -203,9 +205,9 @@ TEST_F(sinsp_with_test_input, plugin_k8s_pod_uid_regex) "b59ce319955234d0b051a93dac5efa8fc07df08d8b0188195b434174efc44e73." "scope"}); init_thread_entry->get_dynamic_field(fieldacc, pod_uid); - // We are not able to extract something valid from the cgroup so we set the - // pod_uid to `""` in the plugin - ASSERT_EQ(pod_uid, ""); + // We are not able to extract something valid from the last call so the + // pod_uid is unchanged + ASSERT_EQ(pod_uid, expected_pod_uid); } // Check that the plugin defines a new field called "pod_uid" in the `init` @@ -403,3 +405,120 @@ TEST_F(sinsp_with_test_input, plugin_k8s_parse_parent_clone) p1_thread_entry->get_dynamic_field(fieldacc, pod_uid); ASSERT_EQ(pod_uid, expected_pod_uid); } + +TEST_F(sinsp_with_test_input, plugin_k8s_proc_scan_in_the_plugin) +{ + std::shared_ptr plugin_owner; + filter_check_list pl_flist; + ASSERT_PLUGIN_INITIALIZATION(plugin_owner, pl_flist) + + // We do a real /proc scan in the plugin while in sinsp we don't do that. + add_default_init_thread(); + open_inspector(); + + // We create a random clone event to trigger the parsing logic. + generate_clone_x_event(0, 2, 2, INIT_TID); + + // The plugin shouldn't crash during the parsing logic of the previous event +} + +#define PLUGIN_PROC_SCAN_UNDER_TMP_PROC \ + int32_t mock_tid = (1 << 16) - 1; \ + std::string mock_proc_dir = "/tmp/proc/" + std::to_string(mock_tid); \ + std::string mock_proc_cgroup = mock_proc_dir + "/cgroup"; \ + std::string expected_pod_uid = "1d34c7bb-7d94-4f00-bed9-fe4eca61d446"; \ + \ + std::error_code ec; \ + std::filesystem::path fullPath = std::filesystem::path(mock_proc_dir); \ + if(!std::filesystem::create_directories(fullPath, ec)) \ + { \ + FAIL() << "unable to create the mock dir: " << mock_proc_dir \ + << ". Error: " << ec.message(); \ + } \ + \ + std::ofstream cgroup_file(mock_proc_cgroup); \ + if(!cgroup_file.is_open()) \ + { \ + FAIL() << "cannot open: " << mock_proc_cgroup \ + << ". Errno: " << strerror(errno); \ + } \ + \ + cgroup_file << "0::/kubepods/besteffort/pod" + expected_pod_uid + \ + "/fc16540dcd776bb475437b722c"; \ + if(cgroup_file.fail()) \ + { \ + FAIL() << "cannot write to: " << mock_proc_cgroup \ + << ". Errno: " << strerror(errno); \ + } \ + cgroup_file.close(); \ + \ + std::string plugin_init_config = \ + "{\"collectorHostname\":\"localhost\",\"collectorPort\": " \ + "45000,\"nodeName\":\"control-plane\",\"verbosity\":" \ + "\"info\", \"hostProc\":\"/tmp\"}"; \ + std::shared_ptr plugin_owner; \ + filter_check_list pl_flist; \ + plugin_owner = m_inspector.register_plugin(PLUGIN_PATH); \ + ASSERT_TRUE(plugin_owner.get()); \ + std::string err; \ + ASSERT_TRUE(plugin_owner->init(plugin_init_config, err)) \ + << "err: " << err; \ + pl_flist.add_filter_check(m_inspector.new_generic_filtercheck()); \ + pl_flist.add_filter_check(sinsp_plugin::new_filtercheck(plugin_owner)); \ + \ + std::filesystem::path mock_proc = std::filesystem::path("/tmp/proc"); \ + ASSERT_EQ(std::filesystem::remove_all(mock_proc), 3); + +TEST_F(sinsp_with_test_input, plugin_k8s_proc_scan_mismatch) +{ + PLUGIN_PROC_SCAN_UNDER_TMP_PROC + + // We do a real /proc scan in the plugin while in sinsp we don't do that. + // Now the plugin cache has one entry while sinps has no entry. we want to + // see how exceptions are handled during the parsing logic. + add_default_init_thread(); + open_inspector(); + + // We create a random clone event to trigger the parsing logic. + generate_clone_x_event(0, 2, 2, INIT_TID); +} + +TEST_F(sinsp_with_test_input, plugin_k8s_pod_uid_population_from_proc) +{ + PLUGIN_PROC_SCAN_UNDER_TMP_PROC + + add_default_init_thread(); + add_simple_thread(mock_tid, mock_tid, INIT_TID); + + // Open test inspector + open_inspector(); + + // we add a new thread manually to the thread table and we check its pod_uid + auto tinfo = m_inspector.get_thread_ref(mock_tid); + ASSERT_TRUE(tinfo); + auto ® = m_inspector.get_table_registry(); + auto thread_table = reg->get_table(THREAD_TABLE_NAME); + auto field = + thread_table->dynamic_fields()->fields().find(POD_UID_FIELD_NAME); + auto fieldacc = field->second.new_accessor(); + + std::string pod_uid = ""; + tinfo->get_dynamic_field(fieldacc, pod_uid); + ASSERT_EQ(pod_uid, ""); + + // Now we try to generate a random event from this thread + // but this is not parsed by the plugin so the pod_uid is not extracted + generate_random_event(mock_tid); + + tinfo->get_dynamic_field(fieldacc, pod_uid); + ASSERT_EQ(pod_uid, ""); + + // Now if we generate an event that is parsed by the plugin + // the pod_uid should be extracted + generate_execve_enter_and_exit_event(0, mock_tid, mock_tid, mock_tid, + INIT_TID); + + // The pod_uid is populated thanks to the plugin internal cache + tinfo->get_dynamic_field(fieldacc, pod_uid); + ASSERT_EQ(pod_uid, expected_pod_uid); +}