mirror of
https://github.com/falcosecurity/falco.git
synced 2025-08-09 10:07:57 +00:00
refactor: adapt event set configuration changes to new libs definition
Co-authored-by: Melissa Kilby <melissa.kilby.oss@gmail.com> Signed-off-by: Jason Dellaluce <jasondellaluce@gmail.com>
This commit is contained in:
parent
01faeecee7
commit
5ed5c63202
@ -25,19 +25,16 @@ FetchContent_MakeAvailable(googletest)
|
|||||||
|
|
||||||
file(GLOB_RECURSE ENGINE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/engine/*.cpp)
|
file(GLOB_RECURSE ENGINE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/engine/*.cpp)
|
||||||
file(GLOB_RECURSE FALCO_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/falco/*.cpp)
|
file(GLOB_RECURSE FALCO_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/falco/*.cpp)
|
||||||
file(GLOB_RECURSE FALCO_APP_ACTIONS_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/falco/app/actions/*.cpp)
|
|
||||||
|
|
||||||
set(FALCO_UNIT_TESTS_SOURCES
|
set(FALCO_UNIT_TESTS_SOURCES
|
||||||
"${ENGINE_TESTS}"
|
"${ENGINE_TESTS}"
|
||||||
"${FALCO_TESTS}"
|
"${FALCO_TESTS}"
|
||||||
"${FALCO_APP_ACTIONS_TESTS}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set(FALCO_UNIT_TESTS_INCLUDES
|
set(FALCO_UNIT_TESTS_INCLUDES
|
||||||
PRIVATE
|
PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/userspace
|
${CMAKE_SOURCE_DIR}/userspace
|
||||||
${CMAKE_BINARY_DIR}/userspace/falco # we need it to include indirectly `config_falco.h` file
|
${CMAKE_BINARY_DIR}/userspace/falco # we need it to include indirectly `config_falco.h` file
|
||||||
${CMAKE_BINARY_DIR}/userspace/falco/app/actions
|
|
||||||
${CMAKE_SOURCE_DIR}/userspace/engine # we need it to include indirectly `falco_common.h` file
|
${CMAKE_SOURCE_DIR}/userspace/engine # we need it to include indirectly `falco_common.h` file
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,6 +24,12 @@ using namespace std;
|
|||||||
using namespace libsinsp::filter;
|
using namespace libsinsp::filter;
|
||||||
using namespace falco::utils;
|
using namespace falco::utils;
|
||||||
|
|
||||||
|
// todo(jasondellaluce): these tests do not test the actual
|
||||||
|
// `configure_interesting_sets` action, but instead reproduces its logic
|
||||||
|
// and asserts the pre and post conditions. For now, this is the only thing
|
||||||
|
// we can do due to the falco_engine class lacking adequate accessor methods.
|
||||||
|
// In the future, we need to refactor this.
|
||||||
|
|
||||||
static std::shared_ptr<gen_event_filter_factory> create_factory()
|
static std::shared_ptr<gen_event_filter_factory> create_factory()
|
||||||
{
|
{
|
||||||
std::shared_ptr<gen_event_filter_factory> ret(new sinsp_filter_factory(NULL));
|
std::shared_ptr<gen_event_filter_factory> ret(new sinsp_filter_factory(NULL));
|
||||||
@ -72,117 +78,74 @@ static std::shared_ptr<filter_ruleset> get_test_rulesets(const std::unordered_se
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
void compare_evttypes_names(const std::unordered_set<std::string>& actual, const std::unordered_set<std::string>& expected)
|
static libsinsp::events::set<ppm_event_code> extract_rules_event_set(std::shared_ptr<filter_ruleset>& r)
|
||||||
{
|
{
|
||||||
|
std::set<uint16_t> tmp;
|
||||||
ASSERT_EQ(actual.size(), expected.size());
|
libsinsp::events::set<ppm_event_code> events;
|
||||||
std::set<std::string> actual_sorted = unordered_set_to_ordered(actual);
|
auto source = falco_common::syscall_source;
|
||||||
std::set<std::string> expected_sorted = unordered_set_to_ordered(expected);
|
r->enabled_evttypes(tmp, 0);
|
||||||
|
for (const auto &ev : tmp)
|
||||||
auto final = actual_sorted.begin();
|
|
||||||
auto matching = expected_sorted.begin();
|
|
||||||
|
|
||||||
for(; final != actual_sorted.end(); final++, matching++)
|
|
||||||
{
|
{
|
||||||
ASSERT_TRUE(*matching == *final);
|
events.insert((ppm_event_code) ev);
|
||||||
}
|
}
|
||||||
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unordered_set<std::string> extract_rules_event_names(std::unique_ptr<sinsp>& inspector, std::shared_ptr<filter_ruleset>& r)
|
#define ASSERT_NAMES_EQ(a, b) { \
|
||||||
{
|
ASSERT_EQ(std::set<std::string>(a.begin(), a.end()), std::set<std::string>(b.begin(), b.end())); \
|
||||||
std::set<uint16_t> rule_events;
|
|
||||||
r->enabled_evttypes(rule_events, 0);
|
|
||||||
std::unordered_set<uint32_t> ppme_events_codes(rule_events.begin(), rule_events.end());
|
|
||||||
return inspector->get_events_names(ppme_events_codes);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_set<std::string> erase_io_syscalls(std::unordered_set<uint32_t> &ppm_sc_of_interest)
|
|
||||||
{
|
|
||||||
std::unordered_set<std::string> erased_io_syscalls_names = {};
|
|
||||||
std::unordered_set<uint32_t> cur_ppm_sc_set = ppm_sc_of_interest;
|
|
||||||
const int bitmask = EC_SYSCALL - 1;
|
|
||||||
for (const auto &ppm_sc_code : cur_ppm_sc_set)
|
|
||||||
{
|
|
||||||
switch(g_infotables.m_syscall_info_table[ppm_sc_code].category & bitmask)
|
|
||||||
{
|
|
||||||
case EC_IO_READ:
|
|
||||||
case EC_IO_WRITE:
|
|
||||||
ppm_sc_of_interest.erase(ppm_sc_code);
|
|
||||||
erased_io_syscalls_names.insert(g_infotables.m_syscall_info_table[ppm_sc_code].name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return erased_io_syscalls_names;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ConfigureInterestingSets, configure_interesting_sets)
|
TEST(ConfigureInterestingSets, configure_interesting_sets)
|
||||||
{
|
{
|
||||||
|
|
||||||
std::unique_ptr<sinsp> inspector(new sinsp());
|
|
||||||
|
|
||||||
/* Test scenario:
|
/* Test scenario:
|
||||||
*
|
|
||||||
* Include one I/O syscall
|
* Include one I/O syscall
|
||||||
* Include one non syscall event type
|
* Include one non syscall event type
|
||||||
* Include one exclusionary syscall definition test ruleset
|
* Include one exclusionary syscall definition test ruleset
|
||||||
* Check sinsp enforced syscalls dependencies for:
|
* Check sinsp enforced syscalls dependencies for:
|
||||||
* - spawned processes
|
* - spawned processes
|
||||||
* - network related syscalls
|
* - network related syscalls
|
||||||
* Check that non syscalls events are enforced
|
* Check that non syscalls events are enforced */
|
||||||
*/
|
|
||||||
std::unordered_set<std::string> fltstrs = {
|
std::unordered_set<std::string> fltstrs = {
|
||||||
"(evt.type=connect or evt.type=accept)",
|
"(evt.type=connect or evt.type=accept)",
|
||||||
"evt.type in (open, ptrace, mmap, execve, read, container)",
|
"evt.type in (open, ptrace, mmap, execve, read, container)",
|
||||||
"evt.type in (open, execve, mprotect) and not evt.type=mprotect"};
|
"evt.type in (open, execve, mprotect) and not evt.type=mprotect"};
|
||||||
std::string test_io_syscall = "read";
|
|
||||||
std::string test_non_syscall = "container";
|
|
||||||
std::unordered_set<std::string> expected_syscalls_names = {
|
std::unordered_set<std::string> expected_syscalls_names = {
|
||||||
"connect", "accept", "open", "ptrace", "mmap", "execve"};
|
"connect", "accept", "open", "ptrace", "mmap", "execve", "read", "container"};
|
||||||
expected_syscalls_names.insert(test_io_syscall);
|
|
||||||
std::unordered_set<std::string> expected_evttypes_names = expected_syscalls_names;
|
|
||||||
expected_evttypes_names.insert(test_non_syscall);
|
|
||||||
std::unordered_set<std::string> base_syscalls_sinsp_state_spawned_process = {"clone", "clone3", "fork", "vfork"};
|
std::unordered_set<std::string> base_syscalls_sinsp_state_spawned_process = {"clone", "clone3", "fork", "vfork"};
|
||||||
std::unordered_set<std::string> base_syscalls_sinsp_state_network = {"socket", "bind", "close"};
|
std::unordered_set<std::string> base_syscalls_sinsp_state_network = {"socket", "bind", "close"};
|
||||||
std::unordered_set<std::string> base_events = {"procexit", "container"};
|
std::unordered_set<std::string> base_events = {"procexit", "container"};
|
||||||
std::unordered_set<std::string> intersection = {};
|
|
||||||
|
|
||||||
auto r = get_test_rulesets(fltstrs);
|
auto r = get_test_rulesets(fltstrs);
|
||||||
ASSERT_EQ(r->enabled_count(0), fltstrs.size());
|
ASSERT_EQ(r->enabled_count(0), fltstrs.size());
|
||||||
|
|
||||||
/* Test if event types names were extracted from each rule in test ruleset. */
|
/* Test if event types names were extracted from each rule in test ruleset. */
|
||||||
std::unordered_set<std::string> rules_evttypes_names = extract_rules_event_names(inspector, r);
|
auto rules_event_set = extract_rules_event_set(r);
|
||||||
compare_evttypes_names(rules_evttypes_names, expected_evttypes_names);
|
auto rules_names = libsinsp::events::event_set_to_names(rules_event_set);
|
||||||
|
ASSERT_NAMES_EQ(rules_names, expected_syscalls_names);
|
||||||
/* Same test again for syscalls events. */
|
|
||||||
std::unordered_set<uint32_t> rules_ppm_sc_set = get_ppm_sc_set_from_syscalls(rules_evttypes_names);
|
|
||||||
std::unordered_set<std::string> rules_syscalls_names = inspector->get_syscalls_names(rules_ppm_sc_set);
|
|
||||||
compare_evttypes_names(rules_syscalls_names, expected_syscalls_names);
|
|
||||||
|
|
||||||
/* Enforce sinsp state syscalls and test if ruleset syscalls are in final set of syscalls. */
|
/* Enforce sinsp state syscalls and test if ruleset syscalls are in final set of syscalls. */
|
||||||
// TODO change to enforce_sinsp_state_ppm_sc
|
auto base_event_set = libsinsp::events::sinsp_state_event_set();
|
||||||
std::unordered_set<uint32_t> ppm_sc_of_interest = inspector->enforce_simple_ppm_sc_set(rules_ppm_sc_set);
|
auto selected_event_set = base_event_set.merge(rules_event_set);
|
||||||
std::unordered_set<std::string> rules_syscalls_names_enforced = inspector->get_syscalls_names(ppm_sc_of_interest);
|
auto selected_names = libsinsp::events::event_set_to_names(selected_event_set);
|
||||||
intersection = unordered_set_intersection(rules_syscalls_names_enforced, expected_syscalls_names);
|
auto intersection = unordered_set_intersection(selected_names, expected_syscalls_names);
|
||||||
compare_evttypes_names(intersection, expected_syscalls_names);
|
ASSERT_NAMES_EQ(intersection, expected_syscalls_names);
|
||||||
|
|
||||||
/* Test if sinsp state enforcement activated required syscalls for test ruleset. */
|
/* Test if sinsp state enforcement activated required syscalls for test ruleset. */
|
||||||
intersection = unordered_set_intersection(rules_syscalls_names_enforced, base_syscalls_sinsp_state_spawned_process);
|
intersection = unordered_set_intersection(selected_names, base_syscalls_sinsp_state_spawned_process);
|
||||||
compare_evttypes_names(intersection, base_syscalls_sinsp_state_spawned_process);
|
ASSERT_NAMES_EQ(intersection, base_syscalls_sinsp_state_spawned_process);
|
||||||
intersection = unordered_set_intersection(rules_syscalls_names_enforced, base_syscalls_sinsp_state_network);
|
intersection = unordered_set_intersection(selected_names, base_syscalls_sinsp_state_network);
|
||||||
compare_evttypes_names(intersection, base_syscalls_sinsp_state_network);
|
ASSERT_NAMES_EQ(intersection, base_syscalls_sinsp_state_network);
|
||||||
|
|
||||||
/* Test that no I/O syscalls are in the final set. */
|
/* Test that no I/O syscalls are in the final set. */
|
||||||
std::unordered_set<uint32_t> io_ppm_sc_set = enforce_io_ppm_sc_set();
|
auto io_event_set = libsinsp::events::sc_set_to_event_set(libsinsp::events::io_sc_set());
|
||||||
std::unordered_set<std::string> erased_io_syscalls_names = inspector->get_syscalls_names(unordered_set_intersection(ppm_sc_of_interest, io_ppm_sc_set));
|
auto erased_event_set = selected_event_set.intersect(io_event_set);
|
||||||
ppm_sc_of_interest = unordered_set_difference(ppm_sc_of_interest, io_ppm_sc_set);
|
selected_event_set = selected_event_set.diff(io_event_set);
|
||||||
rules_syscalls_names_enforced = inspector->get_syscalls_names(ppm_sc_of_interest);
|
selected_names = libsinsp::events::event_set_to_names(selected_event_set);
|
||||||
intersection = unordered_set_intersection(rules_syscalls_names_enforced, erased_io_syscalls_names);
|
intersection = unordered_set_intersection(selected_names, libsinsp::events::event_set_to_names(erased_event_set));
|
||||||
ASSERT_EQ(intersection.size(), 0);
|
ASSERT_EQ(intersection.size(), 0);
|
||||||
|
|
||||||
/* Test that enforced non syscalls events are in final events set. */
|
/* Test that enforced non syscalls events are in final events set. */
|
||||||
std::unordered_set<uint32_t> ppm_event_info_of_interest = inspector->get_event_set_from_ppm_sc_set(ppm_sc_of_interest);
|
intersection = unordered_set_intersection(selected_names, base_events);
|
||||||
ppm_event_info_of_interest = enforce_sinsp_state_ppme(ppm_event_info_of_interest);
|
ASSERT_NAMES_EQ(intersection, base_events);
|
||||||
std::unordered_set<std::string> final_events_names = inspector->get_events_names(ppm_event_info_of_interest);
|
|
||||||
intersection = unordered_set_intersection(final_events_names, base_events);
|
|
||||||
compare_evttypes_names(intersection, base_events);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,201 +16,19 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
#include <sstream>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <unordered_set>
|
|
||||||
#include <set>
|
|
||||||
#include <iterator>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <sinsp.h>
|
|
||||||
|
|
||||||
#include "falco_utils.h"
|
#include "falco_utils.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "banned.h" // This raises a compilation error when certain functions are used
|
#include "banned.h" // This raises a compilation error when certain functions are used
|
||||||
|
|
||||||
extern sinsp_evttables g_infotables;
|
|
||||||
|
|
||||||
namespace falco
|
namespace falco
|
||||||
{
|
{
|
||||||
|
|
||||||
namespace utils
|
namespace utils
|
||||||
{
|
{
|
||||||
|
|
||||||
std::unordered_set<uint32_t> get_ppm_sc_set_from_syscalls(const std::unordered_set<std::string>& syscalls)
|
|
||||||
{
|
|
||||||
std::unordered_set<uint32_t> ppm_sc_set = {};
|
|
||||||
for (int ppm_sc_code = 0; ppm_sc_code < PPM_SC_MAX; ++ppm_sc_code)
|
|
||||||
{
|
|
||||||
std::string ppm_sc_name = g_infotables.m_syscall_info_table[ppm_sc_code].name;
|
|
||||||
if (syscalls.find(ppm_sc_name) != syscalls.end())
|
|
||||||
{
|
|
||||||
ppm_sc_set.insert(ppm_sc_code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ppm_sc_set;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_set<uint32_t> enforce_io_ppm_sc_set(std::unordered_set<uint32_t> ppm_sc_set)
|
|
||||||
{
|
|
||||||
const int bitmask = EC_SYSCALL - 1;
|
|
||||||
for(int ppm_sc_code = 0; ppm_sc_code < PPM_SC_MAX; ppm_sc_code++)
|
|
||||||
{
|
|
||||||
switch(g_infotables.m_syscall_info_table[ppm_sc_code].category & bitmask)
|
|
||||||
{
|
|
||||||
case EC_IO_READ:
|
|
||||||
case EC_IO_WRITE:
|
|
||||||
ppm_sc_set.insert(ppm_sc_code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ppm_sc_set;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_set<uint32_t> enforce_sinsp_state_ppme(std::unordered_set<uint32_t> ppm_event_info_of_interest)
|
|
||||||
{
|
|
||||||
/* Fill-up the set of event infos of interest. This is needed to ensure critical non syscall PPME events are activated, e.g. container or proc exit events. */
|
|
||||||
for (uint32_t ev = 2; ev < PPM_EVENT_MAX; ev++)
|
|
||||||
{
|
|
||||||
if (!sinsp::is_old_version_event(ev)
|
|
||||||
&& !sinsp::is_unused_event(ev)
|
|
||||||
&& !sinsp::is_unknown_event(ev))
|
|
||||||
{
|
|
||||||
/* So far we only covered syscalls, so we add other kinds of
|
|
||||||
interesting events. In this case, we are also interested in
|
|
||||||
metaevents and in the procexit tracepoint event. */
|
|
||||||
if (sinsp::is_metaevent(ev) || ev == PPME_PROCEXIT_1_E)
|
|
||||||
{
|
|
||||||
ppm_event_info_of_interest.insert(ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ppm_event_info_of_interest;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unordered_set_to_ordered
|
|
||||||
template<typename T>
|
|
||||||
std::set<T> unordered_set_to_ordered(const std::unordered_set<T>& unordered_set)
|
|
||||||
{
|
|
||||||
std::set<T> s;
|
|
||||||
for(const auto& val : unordered_set)
|
|
||||||
{
|
|
||||||
s.insert(val);
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
template std::set<uint32_t> unordered_set_to_ordered(const std::unordered_set<uint32_t>& unordered_set);
|
|
||||||
template std::set<std::string> unordered_set_to_ordered(const std::unordered_set<std::string>& unordered_set);
|
|
||||||
|
|
||||||
// unordered_set_difference, equivalent to SQL left_anti join operation
|
|
||||||
template<typename T>
|
|
||||||
std::unordered_set<T> unordered_set_difference(const std::unordered_set<T>& a, const std::unordered_set<T>& b)
|
|
||||||
{
|
|
||||||
std::unordered_set<T> s;
|
|
||||||
for(const auto& val : a)
|
|
||||||
{
|
|
||||||
if (b.find(val) == b.end())
|
|
||||||
{
|
|
||||||
s.insert(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
template std::unordered_set<std::string> unordered_set_difference(const std::unordered_set<std::string>& a, const std::unordered_set<std::string>& b);
|
|
||||||
template std::unordered_set<uint32_t> unordered_set_difference(const std::unordered_set<uint32_t>& a, const std::unordered_set<uint32_t>& b);
|
|
||||||
|
|
||||||
// set_difference, equivalent to SQL left_anti join operation
|
|
||||||
template<typename T>
|
|
||||||
std::set<T> set_difference(const std::set<T>& a, const std::set<T>& b)
|
|
||||||
{
|
|
||||||
std::set<T> out;
|
|
||||||
std::set_difference(a.begin(), a.end(), b.begin(), b.end(), std::inserter(out, out.begin()));
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
template std::set<std::string> set_difference(const std::set<std::string>& a, const std::set<std::string>& b);
|
|
||||||
template std::set<uint32_t> set_difference(const std::set<uint32_t>& a, const std::set<uint32_t>& b);
|
|
||||||
|
|
||||||
// unordered_set_union
|
|
||||||
template<typename T>
|
|
||||||
std::unordered_set<T> unordered_set_union(const std::unordered_set<T>& a, const std::unordered_set<T>& b)
|
|
||||||
{
|
|
||||||
std::unordered_set<T> s = a;
|
|
||||||
for(const auto& val : b)
|
|
||||||
{
|
|
||||||
s.insert(val);
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
template std::unordered_set<std::string> unordered_set_union(const std::unordered_set<std::string>& a, const std::unordered_set<std::string>& b);
|
|
||||||
template std::unordered_set<uint32_t> unordered_set_union(const std::unordered_set<uint32_t>& a, const std::unordered_set<uint32_t>& b);
|
|
||||||
|
|
||||||
// set_union
|
|
||||||
template<typename T>
|
|
||||||
std::set<T> set_union(const std::set<T>& a, const std::set<T>& b)
|
|
||||||
{
|
|
||||||
std::set<T> out;
|
|
||||||
std::set_union(a.begin(), a.end(), b.begin(), b.end(), std::inserter(out, out.begin()));
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
template std::set<std::string> set_union(const std::set<std::string>& a, const std::set<std::string>& b);
|
|
||||||
template std::set<uint32_t> set_union(const std::set<uint32_t>& a, const std::set<uint32_t>& b);
|
|
||||||
|
|
||||||
// unordered_set_intersection
|
|
||||||
template<typename T>
|
|
||||||
std::unordered_set<T> unordered_set_intersection(const std::unordered_set<T>& a, const std::unordered_set<T>& b)
|
|
||||||
{
|
|
||||||
std::unordered_set<T> s;
|
|
||||||
for(const auto& val : a)
|
|
||||||
{
|
|
||||||
if (b.find(val) != b.end())
|
|
||||||
{
|
|
||||||
s.insert(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
template std::unordered_set<std::string> unordered_set_intersection(const std::unordered_set<std::string>& a, const std::unordered_set<std::string>& b);
|
|
||||||
template std::unordered_set<uint32_t> unordered_set_intersection(const std::unordered_set<uint32_t>& a, const std::unordered_set<uint32_t>& b);
|
|
||||||
|
|
||||||
// set_intersection
|
|
||||||
template<typename T>
|
|
||||||
std::set<T> set_intersection(const std::set<T>& a, const std::set<T>& b)
|
|
||||||
{
|
|
||||||
std::set<T> out;
|
|
||||||
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), std::inserter(out, out.begin()));
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
template std::set<std::string> set_intersection(const std::set<std::string>& a, const std::set<std::string>& b);
|
|
||||||
template std::set<uint32_t> set_intersection(const std::set<uint32_t>& a, const std::set<uint32_t>& b);
|
|
||||||
|
|
||||||
std::string concat_set_in_order(const std::unordered_set<std::string>& s, const std::string& delim)
|
|
||||||
{
|
|
||||||
if (s.empty())
|
|
||||||
{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
std::set<std::string> s_ordered = unordered_set_to_ordered(s);
|
|
||||||
std::stringstream ss;
|
|
||||||
std::copy(s_ordered.begin(), s_ordered.end(),
|
|
||||||
std::ostream_iterator<std::string>(ss, delim.c_str()));
|
|
||||||
std::string s_str = ss.str();
|
|
||||||
return s_str.substr(0, s_str.size() - delim.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string concat_set_in_order(const std::set<std::string>& s, const std::string& delim)
|
|
||||||
{
|
|
||||||
if (s.empty())
|
|
||||||
{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
std::stringstream ss;
|
|
||||||
std::copy(s.begin(), s.end(),
|
|
||||||
std::ostream_iterator<std::string>(ss, delim.c_str()));
|
|
||||||
std::string s_str = ss.str();
|
|
||||||
return s_str.substr(0, s_str.size() - delim.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
std::string wrap_text(const std::string& in, uint32_t indent, uint32_t line_len)
|
std::string wrap_text(const std::string& in, uint32_t indent, uint32_t line_len)
|
||||||
{
|
{
|
||||||
std::istringstream is(in);
|
std::istringstream is(in);
|
||||||
|
@ -43,39 +43,6 @@ namespace falco
|
|||||||
namespace utils
|
namespace utils
|
||||||
{
|
{
|
||||||
|
|
||||||
// TODO interim helper methods -> shall be integrated into sinsp APIs
|
|
||||||
std::unordered_set<uint32_t> get_ppm_sc_set_from_syscalls(const std::unordered_set<std::string>& syscalls);
|
|
||||||
std::unordered_set<uint32_t> enforce_sinsp_state_ppme(std::unordered_set<uint32_t> ppm_event_info_of_interest = {});
|
|
||||||
std::unordered_set<uint32_t> enforce_io_ppm_sc_set(std::unordered_set<uint32_t> ppm_sc_set = {}); // needs libs bump hence duplicated in meantime
|
|
||||||
// end interim helper methods
|
|
||||||
|
|
||||||
// TODO interim libs utils methods
|
|
||||||
template<typename T>
|
|
||||||
std::set<T> unordered_set_to_ordered(const std::unordered_set<T>& unordered_set);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
std::unordered_set<T> unordered_set_difference(const std::unordered_set<T>& a, const std::unordered_set<T>& b);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
std::set<T> set_difference(const std::set<T>& a, const std::set<T>& b);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
std::unordered_set<T> unordered_set_union(const std::unordered_set<T>& a, const std::unordered_set<T>& b);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
std::set<T> set_union(const std::set<T>& a, const std::set<T>& b);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
std::unordered_set<T> unordered_set_intersection(const std::unordered_set<T>& a, const std::unordered_set<T>& b);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
std::set<T> set_intersection(const std::set<T>& a, const std::set<T>& b);
|
|
||||||
|
|
||||||
std::string concat_set_in_order(const std::unordered_set<std::string>& s, const std::string& delim = ", ");
|
|
||||||
std::string concat_set_in_order(const std::set<std::string>& s, const std::string& delim = ", ");
|
|
||||||
|
|
||||||
// end interim libs utils methods
|
|
||||||
|
|
||||||
std::string wrap_text(const std::string& in, uint32_t indent, uint32_t linelen);
|
std::string wrap_text(const std::string& in, uint32_t indent, uint32_t linelen);
|
||||||
|
|
||||||
void readfile(const std::string& filename, std::string& data);
|
void readfile(const std::string& filename, std::string& data);
|
||||||
|
@ -25,10 +25,6 @@ namespace actions {
|
|||||||
|
|
||||||
bool check_rules_plugin_requirements(falco::app::state& s, std::string& err);
|
bool check_rules_plugin_requirements(falco::app::state& s, std::string& err);
|
||||||
void print_enabled_event_sources(falco::app::state& s);
|
void print_enabled_event_sources(falco::app::state& s);
|
||||||
void extract_rules_event_names(falco::app::state& s, std::unique_ptr<sinsp>& inspector, std::unordered_set<std::string>& rules_evttypes_names);
|
|
||||||
void activate_interesting_events(falco::app::state& s, std::unique_ptr<sinsp>& inspector, const std::unordered_set<std::string>& rules_evttypes_names);
|
|
||||||
void check_for_unsupported_events(falco::app::state& s, std::unique_ptr<sinsp>& inspector, const std::unordered_set<std::string>& rules_evttypes_names);
|
|
||||||
void activate_interesting_syscalls(falco::app::state& s, std::unique_ptr<sinsp>& inspector, const std::unordered_set<std::string>& rules_evttypes_names);
|
|
||||||
void activate_interesting_kernel_tracepoints(falco::app::state& s, std::unique_ptr<sinsp>& inspector);
|
void activate_interesting_kernel_tracepoints(falco::app::state& s, std::unique_ptr<sinsp>& inspector);
|
||||||
void check_for_ignored_events(falco::app::state& s);
|
void check_for_ignored_events(falco::app::state& s);
|
||||||
void format_plugin_info(std::shared_ptr<sinsp_plugin> p, std::ostream& os);
|
void format_plugin_info(std::shared_ptr<sinsp_plugin> p, std::ostream& os);
|
||||||
|
@ -14,142 +14,137 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
#include "helpers.h"
|
|
||||||
#include "actions.h"
|
#include "actions.h"
|
||||||
#include "falco_utils.h"
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <sinsp.h>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
using namespace falco::app;
|
using namespace falco::app;
|
||||||
using namespace falco::app::actions;
|
using namespace falco::app::actions;
|
||||||
using namespace falco::utils;
|
|
||||||
|
|
||||||
extern sinsp_evttables g_infotables;
|
static libsinsp::events::set<ppm_event_code> extract_rules_event_set(falco::app::state& s)
|
||||||
|
|
||||||
void falco::app::actions::extract_rules_event_names(falco::app::state& s, std::unique_ptr<sinsp>& inspector, std::unordered_set<std::string>& rules_evttypes_names)
|
|
||||||
{
|
{
|
||||||
/* Get all (positive) PPME events from all rules as idx codes.
|
/* Get all (positive) PPME events from all rules as idx codes.
|
||||||
* Events names from negative filter expression statements are NOT included.
|
* Events names from negative filter expression statements are NOT included.
|
||||||
* PPME events in libsinsp are needed to map each event type into it's enter and exit event if applicable (e.g. for syscall events).
|
* PPME events in libsinsp are needed to map each event type into it's enter
|
||||||
*/
|
* and exit event if applicable (e.g. for syscall events). */
|
||||||
std::set<uint16_t> rule_events;
|
std::set<uint16_t> tmp;
|
||||||
std::string source = falco_common::syscall_source;
|
libsinsp::events::set<ppm_event_code> events;
|
||||||
s.engine->evttypes_for_ruleset(source, rule_events);
|
auto source = falco_common::syscall_source;
|
||||||
std::unordered_set<uint32_t> ppme_events_codes(rule_events.begin(), rule_events.end());
|
s.engine->evttypes_for_ruleset(source, tmp);
|
||||||
|
for (const auto &ev : tmp)
|
||||||
/* Translate PPME event idx codes to consolidated event names.
|
{
|
||||||
* Those are the exact event type (evt.type) names from the rules and hence also contain non syscall names, e.g. "container".
|
events.insert((ppm_event_code) ev);
|
||||||
*/
|
}
|
||||||
rules_evttypes_names = inspector->get_events_names(ppme_events_codes);
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
void falco::app::actions::check_for_unsupported_events(falco::app::state& s, std::unique_ptr<sinsp>& inspector, const std::unordered_set<std::string>& rules_evttypes_names)
|
static void check_for_rules_unsupported_events(falco::app::state& s, const libsinsp::events::set<ppm_event_code>& rules_event_set)
|
||||||
{
|
{
|
||||||
std::unordered_set<std::string> intersection = unordered_set_intersection(inspector->get_events_names(s.ppm_event_info_of_interest), rules_evttypes_names);
|
/* Unsupported events are those events that are used in the rules
|
||||||
if(intersection.empty())
|
* but that are not part of the selected event set. For now, this
|
||||||
|
* is expected to happen only for high volume I/O syscalls for
|
||||||
|
* performance reasons. */
|
||||||
|
auto unsupported_event_set = rules_event_set.diff(s.selected_event_set);
|
||||||
|
if (unsupported_event_set.empty())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::unordered_set<std::string> unsupported = unordered_set_difference(rules_evttypes_names, inspector->get_events_names(s.ppm_event_info_of_interest));
|
|
||||||
|
|
||||||
/* Get the names of the events (syscall and non syscall events) that were not activated and print them. */
|
/* Get the names of the events (syscall and non syscall events) that were not activated and print them. */
|
||||||
std::cerr << "Loaded rules match event types that are not activated or unsupported with current configuration: warning (unsupported-evttype): " + concat_set_in_order(unsupported) << std::endl;
|
auto names = libsinsp::events::event_set_to_names(unsupported_event_set);
|
||||||
|
std::cerr << "Loaded rules match event types that are not activated or unsupported with current configuration: warning (unsupported-evttype): " + concat_set_in_order(names) << std::endl;
|
||||||
std::cerr << "If syscalls in rules include high volume I/O syscalls (-> activate via `-A` flag), else (2) syscalls might be associated with syscalls undefined on your architecture (https://marcin.juszkiewicz.com.pl/download/tables/syscalls.html)" << std::endl;
|
std::cerr << "If syscalls in rules include high volume I/O syscalls (-> activate via `-A` flag), else (2) syscalls might be associated with syscalls undefined on your architecture (https://marcin.juszkiewicz.com.pl/download/tables/syscalls.html)" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void falco::app::actions::activate_interesting_events(falco::app::state& s, std::unique_ptr<sinsp>& inspector, const std::unordered_set<std::string>& rules_evttypes_names)
|
static void select_event_set(falco::app::state& s, const libsinsp::events::set<ppm_event_code>& rules_event_set)
|
||||||
{
|
{
|
||||||
std::unordered_set<uint32_t> ppm_event_info_of_interest = inspector->get_event_set_from_ppm_sc_set(s.ppm_sc_of_interest);
|
/* PPM syscall codes (sc) can be viewed as condensed libsinsp lookup table
|
||||||
s.ppm_event_info_of_interest = enforce_sinsp_state_ppme(ppm_event_info_of_interest);
|
* to map a system call name to it's actual system syscall id (as defined
|
||||||
check_for_unsupported_events(s, inspector, rules_evttypes_names);
|
* by the Linux kernel). Hence here we don't need syscall enter and exit distinction. */
|
||||||
|
auto rules_names = libsinsp::events::event_set_to_names(rules_event_set);
|
||||||
|
if (!rules_event_set.empty())
|
||||||
|
{
|
||||||
|
falco_logger::log(LOG_DEBUG, "(" + std::to_string(rules_names.size())
|
||||||
|
+ ") syscalls activated in rules: " + concat_set_in_order(rules_names) + "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void falco::app::actions::activate_interesting_syscalls(falco::app::state& s, std::unique_ptr<sinsp>& inspector, const std::unordered_set<std::string>& rules_evttypes_names)
|
/* DEFAULT OPTION:
|
||||||
{
|
* Current sinsp_state_sc_set() approach includes multiple steps:
|
||||||
|
|
||||||
/* Translate PPME event names to PPM syscall idx codes.
|
|
||||||
* PPM syscall idx codes can be viewed as condensed libsinsp lookup table to map a system call name to it's actual system syscall id (as defined by the Linux kernel).
|
|
||||||
* Hence here we don't need syscall enter and exit distinction.
|
|
||||||
*/
|
|
||||||
std::unordered_set<uint32_t> rules_ppm_sc_set = get_ppm_sc_set_from_syscalls(rules_evttypes_names);
|
|
||||||
std::unordered_set<std::string> rules_syscalls_names = inspector->get_syscalls_names(rules_ppm_sc_set);
|
|
||||||
if (rules_syscalls_names.size() > 0)
|
|
||||||
{
|
|
||||||
falco_logger::log(LOG_DEBUG, "(" + std::to_string(rules_syscalls_names.size()) + ") syscalls activated in rules: " + concat_set_in_order(rules_syscalls_names) + "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
*
|
|
||||||
* DEFAULT OPTION:
|
|
||||||
*
|
|
||||||
* Current enforce_simple_ppm_sc_set approach includes multiple steps:
|
|
||||||
* (1) Enforce all positive syscalls from each Falco rule
|
* (1) Enforce all positive syscalls from each Falco rule
|
||||||
* (2) Enforce a static set of syscalls in addition to the syscalls defined in Falco's rules
|
* (2) Enforce `libsinsp` state set (non-adaptive, not conditioned by rules,
|
||||||
* (3) Enforce `libsinsp` state set (non-adaptive, not conditioned by rules, but based on PPME event table flags indicating generic sinsp state modifications)
|
but based on PPME event table flags indicating generic sinsp state modifications)
|
||||||
* -> Final set is union of (1), (2) and (3)
|
* -> Final set is union of (1) and (2) */
|
||||||
*
|
auto base_event_set = libsinsp::events::sinsp_state_event_set();
|
||||||
*/
|
s.selected_event_set = rules_event_set.merge(base_event_set);
|
||||||
|
|
||||||
// TODO change to enforce_sinsp_state_ppm_sc
|
/* Derive the diff between the additional syscalls added via libsinsp state
|
||||||
s.ppm_sc_of_interest = inspector->enforce_simple_ppm_sc_set(rules_ppm_sc_set);
|
enforcement and the syscalls from each Falco rule. */
|
||||||
|
auto non_rules_event_set = s.selected_event_set.diff(rules_event_set);
|
||||||
/* Derive the diff between the additional syscalls added via libsinsp state enforcement and the syscalls from each Falco rule. */
|
if (!non_rules_event_set.empty())
|
||||||
std::unordered_set<std::string> non_rules_syscalls_names = unordered_set_difference(inspector->get_syscalls_names(s.ppm_sc_of_interest), rules_syscalls_names);
|
|
||||||
|
|
||||||
if (non_rules_syscalls_names.size() > 0)
|
|
||||||
{
|
{
|
||||||
falco_logger::log(LOG_DEBUG, "+(" + std::to_string(non_rules_syscalls_names.size()) + ") syscalls activated (Falco's set of additional syscalls including syscalls needed for state engine): " + concat_set_in_order(non_rules_syscalls_names) + "\n");
|
falco_logger::log(LOG_DEBUG, "+(" + std::to_string(non_rules_event_set.size())
|
||||||
|
+ ") syscalls activated (Falco's set of additional syscalls including syscalls needed for state engine): "
|
||||||
|
+ concat_set_in_order(libsinsp::events::event_set_to_names(non_rules_event_set)) + "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -A flag behavior:
|
/* -A flag behavior:
|
||||||
* default: all syscalls in rules included, sinsp state enforcement without high volume I/O syscalls
|
* (1) default: all syscalls in rules included, sinsp state enforcement
|
||||||
* -A flag set: all syscalls in rules included, sinsp state enforcement and allowing high volume I/O syscalls
|
without high volume I/O syscalls
|
||||||
*/
|
* (2) -A flag set: all syscalls in rules included, sinsp state enforcement
|
||||||
|
and allowing high volume I/O syscalls */
|
||||||
if(!s.options.all_events)
|
if(!s.options.all_events)
|
||||||
{
|
{
|
||||||
std::unordered_set<uint32_t> io_ppm_sc_set = enforce_io_ppm_sc_set();
|
auto ignored_event_set = libsinsp::events::sc_set_to_event_set(libsinsp::events::io_sc_set());
|
||||||
std::unordered_set<std::string> erased_io_syscalls_names = inspector->get_syscalls_names(unordered_set_intersection(s.ppm_sc_of_interest, io_ppm_sc_set));
|
auto erased_event_set = s.selected_event_set.intersect(ignored_event_set);
|
||||||
s.ppm_sc_of_interest = unordered_set_difference(s.ppm_sc_of_interest, io_ppm_sc_set);
|
s.selected_event_set = s.selected_event_set.diff(ignored_event_set);
|
||||||
|
if (!erased_event_set.empty())
|
||||||
if (erased_io_syscalls_names.size() > 0)
|
|
||||||
{
|
{
|
||||||
falco_logger::log(LOG_DEBUG, "-(" + std::to_string(erased_io_syscalls_names.size()) + ") high volume I/O syscalls (`-A` flag not set): " + concat_set_in_order(erased_io_syscalls_names) + "\n");
|
falco_logger::log(LOG_DEBUG, "-(" + std::to_string(erased_event_set.size())
|
||||||
|
+ ") ignored high volume I/O syscalls (`-A` flag not set): "
|
||||||
|
+ concat_set_in_order(libsinsp::events::event_set_to_names(erased_event_set)) + "\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unordered_set<std::string> final_syscalls_names = inspector->get_syscalls_names(s.ppm_sc_of_interest);
|
if (!s.selected_event_set.empty())
|
||||||
|
|
||||||
if (final_syscalls_names.size() > 0)
|
|
||||||
{
|
{
|
||||||
falco_logger::log(LOG_DEBUG, "(" + std::to_string(final_syscalls_names.size()) + ") syscalls in total activated (final set): " + concat_set_in_order(final_syscalls_names) + "\n");
|
falco_logger::log(LOG_DEBUG, "(" + std::to_string(s.selected_event_set.size())
|
||||||
|
+ ") syscalls in total activated (final set): "
|
||||||
|
+ concat_set_in_order(libsinsp::events::event_set_to_names(s.selected_event_set)) + "\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void select_syscall_set(falco::app::state& s, const libsinsp::events::set<ppm_event_code>& rules_event_set)
|
||||||
|
{
|
||||||
|
s.selected_sc_set = libsinsp::events::event_set_to_sc_set(s.selected_event_set);
|
||||||
}
|
}
|
||||||
|
|
||||||
void falco::app::actions::activate_interesting_kernel_tracepoints(falco::app::state& s, std::unique_ptr<sinsp>& inspector)
|
static void select_kernel_tracepoint_set(falco::app::state& s)
|
||||||
{
|
{
|
||||||
/* Kernel tracepoints activation
|
/* Kernel tracepoints activation
|
||||||
*
|
* Activate all tracepoints except `sched_switch` tracepoint since it
|
||||||
* Activate all tracepoints except `sched_switch` tracepoint since it is highly noisy and not so useful
|
* is highly noisy and not so useful
|
||||||
* for our state/events enrichment.
|
* for our state/events enrichment. */
|
||||||
*/
|
|
||||||
s.selected_tp_set = libsinsp::events::sinsp_state_tp_set();
|
s.selected_tp_set = libsinsp::events::sinsp_state_tp_set();
|
||||||
s.selected_tp_set.remove(ppm_tp_code::SCHED_SWITCH);
|
s.selected_tp_set.remove(ppm_tp_code::SCHED_SWITCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
falco::app::run_result falco::app::actions::configure_interesting_sets(falco::app::state& s)
|
falco::app::run_result falco::app::actions::configure_interesting_sets(falco::app::state& s)
|
||||||
{
|
{
|
||||||
|
s.selected_event_set.clear();
|
||||||
|
s.selected_sc_set.clear();
|
||||||
|
s.selected_tp_set.clear();
|
||||||
|
|
||||||
std::unique_ptr<sinsp> inspector(new sinsp());
|
/* note: the set of events is the richest source of truth about
|
||||||
std::unordered_set<std::string> rules_evttypes_names;
|
* the events generable by an inspector, because they also carry information
|
||||||
|
* about events that are old, unused, internal, and so on. As such, the
|
||||||
falco::app::actions::extract_rules_event_names(s, inspector, rules_evttypes_names); // when reaching this code all evttypes are valid
|
* strategy is to first craft the actual set of selected events, and
|
||||||
falco::app::actions::activate_interesting_syscalls(s, inspector, rules_evttypes_names);
|
* then use it to obtain a set of enabled kernel tracepoints and a set
|
||||||
falco::app::actions::activate_interesting_events(s, inspector, rules_evttypes_names);
|
* of syscall codes. Those last two sets will be passed down to the
|
||||||
falco::app::actions::activate_interesting_kernel_tracepoints(s, inspector);
|
* inspector to instruct the kernel drivers on which kernel event should
|
||||||
|
* be collected at runtime. */
|
||||||
|
auto rules_event_set = extract_rules_event_set(s);
|
||||||
|
select_event_set(s, rules_event_set);
|
||||||
|
check_for_rules_unsupported_events(s, rules_event_set);
|
||||||
|
select_syscall_set(s, rules_event_set);
|
||||||
|
select_kernel_tracepoint_set(s);
|
||||||
return run_result::ok();
|
return run_result::ok();
|
||||||
}
|
}
|
||||||
|
@ -25,17 +25,13 @@ using namespace falco::utils;
|
|||||||
|
|
||||||
falco::app::run_result falco::app::actions::print_ignored_events(falco::app::state& s)
|
falco::app::run_result falco::app::actions::print_ignored_events(falco::app::state& s)
|
||||||
{
|
{
|
||||||
|
|
||||||
if(!s.options.print_ignored_events)
|
if(!s.options.print_ignored_events)
|
||||||
{
|
{
|
||||||
return run_result::ok();
|
return run_result::ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<sinsp> inspector(new sinsp());
|
|
||||||
std::unordered_set<uint32_t> io_ppm_sc_set = enforce_io_ppm_sc_set();
|
|
||||||
|
|
||||||
std::cout << "Ignored I/O syscall(s):" << std::endl;
|
std::cout << "Ignored I/O syscall(s):" << std::endl;
|
||||||
for(const auto& it : inspector->get_syscalls_names(io_ppm_sc_set))
|
for(const auto& it : libsinsp::events::sc_set_to_names(libsinsp::events::io_sc_set()))
|
||||||
{
|
{
|
||||||
std::cout << "- " << it.c_str() << std::endl;
|
std::cout << "- " << it.c_str() << std::endl;
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ falco::app::run_result falco::app::actions::print_syscall_events(falco::app::sta
|
|||||||
{
|
{
|
||||||
if(s.options.list_syscall_events)
|
if(s.options.list_syscall_events)
|
||||||
{
|
{
|
||||||
const auto events = get_event_entries(true, s.ppm_event_info_of_interest);
|
const auto events = get_event_entries(true, libsinsp::events::all_event_set());
|
||||||
|
|
||||||
if(s.options.markdown)
|
if(s.options.markdown)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user