mirror of
https://github.com/falcosecurity/falco.git
synced 2026-03-18 18:58:41 +00:00
wip
Co-authored-by: Lorenzo Fontana <lo@linux.com> Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com> Signed-off-by: Lorenzo Fontana <lo@linux.com> Signed-off-by: Leonardo Di Donato <leodidonato@gmail.com>
This commit is contained in:
@@ -2425,9 +2425,9 @@
|
||||
- rule: Contact K8S API Server From Container
|
||||
desc: Detect attempts to contact the K8S API Server from a container
|
||||
condition: >
|
||||
evt.type=connect and evt.dir=< and
|
||||
evt.type=connect and evt.dir=< and
|
||||
(fd.typechar=4 or fd.typechar=6) and
|
||||
container and
|
||||
container and
|
||||
not k8s_containers and
|
||||
k8s_api_server and
|
||||
not user_known_contact_k8s_api_server_activities
|
||||
@@ -2647,7 +2647,7 @@
|
||||
- rule: Delete or rename shell history
|
||||
desc: Detect shell history deletion
|
||||
condition: >
|
||||
(modify_shell_history or truncate_shell_history) and
|
||||
(modify_shell_history or truncate_shell_history) and
|
||||
not var_lib_docker_filepath and
|
||||
not proc.name in (docker_binaries)
|
||||
output: >
|
||||
@@ -2881,7 +2881,7 @@
|
||||
tags: [container, mitre_execution]
|
||||
|
||||
|
||||
# This rule is enabled by default.
|
||||
# This rule is enabled by default.
|
||||
# If you want to disable it, modify the following macro.
|
||||
- macro: consider_packet_socket_communication
|
||||
condition: (always_true)
|
||||
|
||||
@@ -51,11 +51,11 @@ falco_engine::falco_engine(bool seed_rng, const std::string &alternate_lua_dir):
|
||||
luaopen_lpeg(m_ls);
|
||||
luaopen_yaml(m_ls);
|
||||
|
||||
m_alternate_lua_dir = alternate_lua_dir;
|
||||
falco_common::init(m_lua_main_filename.c_str(), alternate_lua_dir.c_str());
|
||||
falco_rules::init(m_ls);
|
||||
|
||||
m_sinsp_rules.reset(new falco_sinsp_ruleset());
|
||||
m_k8s_audit_rules.reset(new falco_ruleset());
|
||||
clear_filters();
|
||||
|
||||
if(seed_rng)
|
||||
{
|
||||
@@ -66,8 +66,6 @@ falco_engine::falco_engine(bool seed_rng, const std::string &alternate_lua_dir):
|
||||
|
||||
// Create this now so we can potentially list filters and exit
|
||||
m_json_factory = make_shared<json_event_filter_factory>();
|
||||
|
||||
hawk_init();
|
||||
}
|
||||
|
||||
falco_engine::~falco_engine()
|
||||
@@ -76,7 +74,15 @@ falco_engine::~falco_engine()
|
||||
{
|
||||
delete m_rules;
|
||||
}
|
||||
hawk_destroy();
|
||||
}
|
||||
|
||||
falco_engine *falco_engine::clone()
|
||||
{
|
||||
auto engine = new falco_engine(true, m_alternate_lua_dir);
|
||||
engine->set_inspector(m_inspector);
|
||||
engine->set_extra(m_extra, m_replace_container_info);
|
||||
engine->set_min_priority(m_min_priority);
|
||||
return engine;
|
||||
}
|
||||
|
||||
uint32_t falco_engine::engine_version()
|
||||
@@ -180,32 +186,52 @@ void falco_engine::load_rules(const string &rules_content, bool verbose, bool al
|
||||
|
||||
if(!m_rules)
|
||||
{
|
||||
m_rules = new falco_rules(m_inspector,
|
||||
this,
|
||||
m_ls);
|
||||
// Note that falco_formats is added to the lua state used by the falco engine only.
|
||||
// Within the engine, only formats.
|
||||
// Formatter is used, so we can unconditionally set json_output to false.
|
||||
bool json_output = false;
|
||||
bool json_include_output_property = false;
|
||||
falco_formats::init(m_inspector, this, m_ls, json_output, json_include_output_property);
|
||||
m_rules = new falco_rules(m_inspector, this, m_ls);
|
||||
}
|
||||
|
||||
// Note that falco_formats is added to the lua state used
|
||||
// by the falco engine only. Within the engine, only
|
||||
// formats.formatter is used, so we can unconditionally set
|
||||
// json_output to false.
|
||||
bool json_output = false;
|
||||
bool json_include_output_property = false;
|
||||
falco_formats::init(m_inspector, this, m_ls, json_output, json_include_output_property);
|
||||
uint64_t dummy;
|
||||
// m_sinsp_rules.reset(new falco_sinsp_ruleset());
|
||||
// m_k8s_audit_rules.reset(new falco_ruleset());
|
||||
m_rules->load_rules(rules_content, verbose, all_events, m_extra, m_replace_container_info, m_min_priority, dummy);
|
||||
|
||||
m_is_ready = true;
|
||||
|
||||
return;
|
||||
|
||||
//
|
||||
// auto local_rules = new falco_rules(m_inspector, this, m_ls);
|
||||
// try
|
||||
// {
|
||||
// uint64_t dummy;
|
||||
// local_rules->load_rules(rules_content, verbose, all_events, m_extra, m_replace_container_info, m_min_priority, dummy);
|
||||
|
||||
// // m_rules = local_rules
|
||||
// // std::atomic<falco_rules *> lore(m_rules);
|
||||
// // std::atomic_exchange(&lore, local_rules);
|
||||
// // SCHEDULE LOCAL_RULES AS NEXT RULESET
|
||||
// }
|
||||
// catch(const falco_exception &e)
|
||||
// {
|
||||
// // todo
|
||||
// printf("IGNORE BECAUSE OF ERROR LOADING RULESET!\n");
|
||||
// }
|
||||
}
|
||||
|
||||
// todo(fntlnz): make this do the real loading
|
||||
static void rules_cb(char *rules_content, hawk_engine *engine)
|
||||
{
|
||||
reinterpret_cast<falco_engine *>(engine)->load_rules(rules_content, false, true);
|
||||
}
|
||||
// // todo(fntlnz): not sure we want this in falco_engine
|
||||
// void falco_engine::watch_rules(bool verbose, bool all_events)
|
||||
// {
|
||||
// hawk_watch_rules((hawk_watch_rules_cb)rules_cb, reinterpret_cast<hawk_engine *>(this));
|
||||
// }
|
||||
|
||||
// todo(fntlnz): not sure we want this in falco_engine
|
||||
void falco_engine::watch_rules(bool verbose, bool all_events)
|
||||
bool falco_engine::is_ready()
|
||||
{
|
||||
hawk_watch_rules((hawk_watch_rules_cb)rules_cb, reinterpret_cast<hawk_engine *>(this));
|
||||
return m_is_ready;
|
||||
}
|
||||
|
||||
void falco_engine::enable_rule(const string &substring, bool enabled, const string &ruleset)
|
||||
@@ -334,6 +360,7 @@ unique_ptr<falco_engine::rule_result> falco_engine::process_sinsp_event(sinsp_ev
|
||||
|
||||
unique_ptr<falco_engine::rule_result> falco_engine::process_sinsp_event(sinsp_evt *ev)
|
||||
{
|
||||
// todo(leodido, fntlnz) > pass the last ruleset id
|
||||
return process_sinsp_event(ev, m_default_ruleset_id);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,12 @@ extern "C"
|
||||
class falco_engine : public falco_common
|
||||
{
|
||||
public:
|
||||
falco_engine(bool seed_rng=true, const std::string& alternate_lua_dir=FALCO_ENGINE_SOURCE_LUA_DIR);
|
||||
falco_engine(bool seed_rng = true, const std::string &alternate_lua_dir = FALCO_ENGINE_SOURCE_LUA_DIR);
|
||||
virtual ~falco_engine();
|
||||
|
||||
falco_engine(const falco_engine &rhs);
|
||||
falco_engine *clone();
|
||||
|
||||
// A given engine has a version which identifies the fields
|
||||
// and rules file format it supports. This version will change
|
||||
// any time the code that handles rules files, expression
|
||||
@@ -62,7 +65,7 @@ public:
|
||||
static uint32_t engine_version();
|
||||
|
||||
// Print to stdout (using printf) a description of each field supported by this engine.
|
||||
void list_fields(bool names_only=false);
|
||||
void list_fields(bool names_only = false);
|
||||
|
||||
//
|
||||
// Load rules either directly or from a filename.
|
||||
@@ -86,7 +89,6 @@ public:
|
||||
// Wrapper that assumes the default ruleset
|
||||
void enable_rule(const std::string &substring, bool enabled);
|
||||
|
||||
|
||||
// Like enable_rule, but the rule name must be an exact match.
|
||||
void enable_rule_exact(const std::string &rule_name, bool enabled, const std::string &ruleset);
|
||||
|
||||
@@ -155,7 +157,8 @@ public:
|
||||
|
||||
// **Methods Related to k8s audit log events, which are
|
||||
// **represented as json objects.
|
||||
struct rule_result {
|
||||
struct rule_result
|
||||
{
|
||||
gen_event *evt;
|
||||
std::string rule;
|
||||
std::string source;
|
||||
@@ -171,7 +174,7 @@ public:
|
||||
// Returns true if the json object was recognized as a k8s
|
||||
// audit event(s), false otherwise.
|
||||
//
|
||||
bool parse_k8s_audit_json(nlohmann::json &j, std::list<json_event> &evts, bool top=true);
|
||||
bool parse_k8s_audit_json(nlohmann::json &j, std::list<json_event> &evts, bool top = true);
|
||||
|
||||
//
|
||||
// Given an event, check it against the set of rules in the
|
||||
@@ -196,7 +199,7 @@ public:
|
||||
//
|
||||
void add_k8s_audit_filter(std::string &rule,
|
||||
std::set<std::string> &tags,
|
||||
json_event_filter* filter);
|
||||
json_event_filter *filter);
|
||||
|
||||
// **Methods Related to Sinsp Events e.g system calls
|
||||
//
|
||||
@@ -237,13 +240,14 @@ public:
|
||||
std::set<uint32_t> &evttypes,
|
||||
std::set<uint32_t> &syscalls,
|
||||
std::set<std::string> &tags,
|
||||
sinsp_filter* filter);
|
||||
sinsp_filter *filter);
|
||||
|
||||
sinsp_filter_factory &sinsp_factory();
|
||||
json_event_filter_factory &json_factory();
|
||||
|
||||
private:
|
||||
bool is_ready();
|
||||
|
||||
private:
|
||||
static nlohmann::json::json_pointer k8s_audit_time;
|
||||
|
||||
//
|
||||
@@ -263,6 +267,8 @@ private:
|
||||
std::unique_ptr<falco_sinsp_ruleset> m_sinsp_rules;
|
||||
std::unique_ptr<falco_ruleset> m_k8s_audit_rules;
|
||||
|
||||
std::string m_alternate_lua_dir;
|
||||
|
||||
//
|
||||
// Here's how the sampling ratio and multiplier influence
|
||||
// whether or not an event is dropped in
|
||||
@@ -292,5 +298,6 @@ private:
|
||||
|
||||
std::string m_extra;
|
||||
bool m_replace_container_info;
|
||||
};
|
||||
|
||||
bool m_is_ready = false;
|
||||
};
|
||||
|
||||
@@ -30,6 +30,8 @@ limitations under the License.
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <getopt.h>
|
||||
#include <condition_variable>
|
||||
#include <tuple>
|
||||
|
||||
#include <sinsp.h>
|
||||
|
||||
@@ -56,6 +58,10 @@ bool g_reopen_outputs = false;
|
||||
bool g_restart = false;
|
||||
bool g_daemonized = false;
|
||||
|
||||
std::mutex engine_ready;
|
||||
std::condition_variable engine_cv;
|
||||
bool is_engine_ready = false;
|
||||
|
||||
//
|
||||
// Helper functions
|
||||
//
|
||||
@@ -232,8 +238,7 @@ static std::string read_file(std::string filename)
|
||||
//
|
||||
// Event processing loop
|
||||
//
|
||||
uint64_t do_inspect(falco_engine *engine,
|
||||
falco_outputs *outputs,
|
||||
uint64_t do_inspect(falco_engine *engine, falco_outputs *outputs,
|
||||
sinsp *inspector,
|
||||
falco_configuration &config,
|
||||
syscall_evt_drop_mgr &sdropmgr,
|
||||
@@ -243,6 +248,7 @@ uint64_t do_inspect(falco_engine *engine,
|
||||
bool all_events,
|
||||
int &result)
|
||||
{
|
||||
|
||||
uint64_t num_evts = 0;
|
||||
int32_t rc;
|
||||
sinsp_evt *ev;
|
||||
@@ -266,12 +272,23 @@ uint64_t do_inspect(falco_engine *engine,
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// wait for the first engine to be ready
|
||||
std::unique_lock<std::mutex> lk(engine_ready);
|
||||
engine_cv.wait(lk, [] { return is_engine_ready; });
|
||||
}
|
||||
|
||||
// printf("ADDRESS: %p\n", engine);
|
||||
|
||||
//
|
||||
// Loop through the events
|
||||
//
|
||||
|
||||
std::atomic<falco_engine *> e;
|
||||
falco_engine *h = e.load();
|
||||
falco_engine *engine_to_use = nullptr;
|
||||
while(1)
|
||||
{
|
||||
|
||||
rc = inspector->next(&ev);
|
||||
|
||||
writer.handle();
|
||||
@@ -338,7 +355,16 @@ uint64_t do_inspect(falco_engine *engine,
|
||||
// engine, which will match the event against the set
|
||||
// of rules. If a match is found, pass the event to
|
||||
// the outputs.
|
||||
unique_ptr<falco_engine::rule_result> res = engine->process_sinsp_event(ev);
|
||||
bool engine_cmp_res = e.compare_exchange_strong(h, engine);
|
||||
if(engine_cmp_res == true)
|
||||
{
|
||||
engine_to_use = e.load();
|
||||
}
|
||||
if(engine_to_use == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
unique_ptr<falco_engine::rule_result> res = engine_to_use->process_sinsp_event(ev);
|
||||
if(res)
|
||||
{
|
||||
outputs->handle_event(res->evt, res->rule, res->source, res->priority_num, res->format);
|
||||
@@ -408,6 +434,23 @@ static void list_source_fields(falco_engine *engine, bool verbose, bool names_on
|
||||
}
|
||||
}
|
||||
|
||||
static void rules_cb(char *rules_content, hawk_engine &engine)
|
||||
{
|
||||
auto &x = reinterpret_cast<falco_engine *&>(engine);
|
||||
|
||||
x = x->clone();
|
||||
x->load_rules(rules_content, false, true);
|
||||
|
||||
engine = x;
|
||||
|
||||
if(!is_engine_ready)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(engine_ready);
|
||||
is_engine_ready = true;
|
||||
engine_cv.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// ARGUMENT PARSING AND PROGRAM SETUP
|
||||
//
|
||||
@@ -416,8 +459,10 @@ int falco_init(int argc, char **argv)
|
||||
int result = EXIT_SUCCESS;
|
||||
sinsp *inspector = NULL;
|
||||
sinsp_evt::param_fmt event_buffer_format = sinsp_evt::PF_NORMAL;
|
||||
falco_engine *engine = NULL;
|
||||
std::thread engine_watchrules_thread;
|
||||
// std::promise<std::shared_ptr<falco_engine>> engine;
|
||||
// std::future<std::shared_ptr<falco_engine>> engine_future = engine.get_future();
|
||||
falco_engine *bluengine;
|
||||
std::thread watchrules_thread;
|
||||
falco_outputs *outputs = NULL;
|
||||
syscall_evt_drop_mgr sdropmgr;
|
||||
int op;
|
||||
@@ -729,15 +774,15 @@ int falco_init(int argc, char **argv)
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
engine = new falco_engine(true, alternate_lua_dir);
|
||||
engine->set_inspector(inspector);
|
||||
engine->set_extra(output_format, replace_container_info);
|
||||
bluengine = new falco_engine(true, alternate_lua_dir);
|
||||
bluengine->set_inspector(inspector);
|
||||
bluengine->set_extra(output_format, replace_container_info);
|
||||
|
||||
if(list_flds)
|
||||
{
|
||||
list_source_fields(engine, verbose, names_only, list_flds_source);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
// if(list_flds)
|
||||
// {
|
||||
// list_source_fields(engine, verbose, names_only, list_flds_source);
|
||||
// return EXIT_SUCCESS;
|
||||
// }
|
||||
|
||||
if(disable_sources.size() > 0)
|
||||
{
|
||||
@@ -798,31 +843,31 @@ int falco_init(int argc, char **argv)
|
||||
}
|
||||
|
||||
// validate the rules files and exit
|
||||
if(validate_rules_filenames.size() > 0)
|
||||
{
|
||||
falco_logger::log(LOG_INFO, "Validating rules file(s):\n");
|
||||
for(auto file : validate_rules_filenames)
|
||||
{
|
||||
falco_logger::log(LOG_INFO, " " + file + "\n");
|
||||
}
|
||||
for(auto file : validate_rules_filenames)
|
||||
{
|
||||
// Only include the prefix if there is more than one file
|
||||
std::string prefix = (validate_rules_filenames.size() > 1 ? file + ": " : "");
|
||||
try
|
||||
{
|
||||
engine->load_rules_file(file, verbose, all_events);
|
||||
}
|
||||
catch(falco_exception &e)
|
||||
{
|
||||
printf("%s%s\n", prefix.c_str(), e.what());
|
||||
throw;
|
||||
}
|
||||
printf("%sOk\n", prefix.c_str());
|
||||
}
|
||||
falco_logger::log(LOG_INFO, "Ok\n");
|
||||
goto exit;
|
||||
}
|
||||
// if(validate_rules_filenames.size() > 0)
|
||||
// {
|
||||
// falco_logger::log(LOG_INFO, "Validating rules file(s):\n");
|
||||
// for(auto file : validate_rules_filenames)
|
||||
// {
|
||||
// falco_logger::log(LOG_INFO, " " + file + "\n");
|
||||
// }
|
||||
// for(auto file : validate_rules_filenames)
|
||||
// {
|
||||
// // Only include the prefix if there is more than one file
|
||||
// std::string prefix = (validate_rules_filenames.size() > 1 ? file + ": " : "");
|
||||
// try
|
||||
// {
|
||||
// engine->load_rules_file(file, verbose, all_events);
|
||||
// }
|
||||
// catch(falco_exception &e)
|
||||
// {
|
||||
// printf("%s%s\n", prefix.c_str(), e.what());
|
||||
// throw;
|
||||
// }
|
||||
// printf("%sOk\n", prefix.c_str());
|
||||
// }
|
||||
// falco_logger::log(LOG_INFO, "Ok\n");
|
||||
// goto exit;
|
||||
// }
|
||||
|
||||
falco_configuration config;
|
||||
if(conf_filename.size())
|
||||
@@ -844,15 +889,17 @@ int falco_init(int argc, char **argv)
|
||||
falco_logger::log(LOG_INFO, "Falco initialized. No configuration file found, proceeding with defaults\n");
|
||||
}
|
||||
|
||||
engine->set_min_priority(config.m_min_priority);
|
||||
bluengine->set_min_priority(config.m_min_priority);
|
||||
|
||||
if(buffered_cmdline)
|
||||
{
|
||||
config.m_buffered_outputs = buffered_outputs;
|
||||
}
|
||||
|
||||
engine_watchrules_thread = std::thread([&engine, verbose, all_events] {
|
||||
engine->watch_rules(verbose, all_events);
|
||||
hawk_init();
|
||||
watchrules_thread = std::thread([&] {
|
||||
// todo: pass verbose, and all_events
|
||||
hawk_watch_rules((hawk_watch_rules_cb)rules_cb, reinterpret_cast<hawk_engine *>(&bluengine));
|
||||
});
|
||||
|
||||
falco_logger::log(LOG_INFO, "DOPO\n");
|
||||
@@ -958,13 +1005,13 @@ int falco_init(int argc, char **argv)
|
||||
|
||||
if(describe_all_rules)
|
||||
{
|
||||
engine->describe_rule(NULL);
|
||||
// engine->describe_rule(NULL);
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if(describe_rule != "")
|
||||
{
|
||||
engine->describe_rule(&describe_rule);
|
||||
// engine->describe_rule(&describe_rule);
|
||||
goto exit;
|
||||
}
|
||||
|
||||
@@ -1247,10 +1294,10 @@ int falco_init(int argc, char **argv)
|
||||
|
||||
if(trace_filename.empty() && config.m_webserver_enabled && !disable_k8s_audit)
|
||||
{
|
||||
std::string ssl_option = (config.m_webserver_ssl_enabled ? " (SSL)" : "");
|
||||
falco_logger::log(LOG_INFO, "Starting internal webserver, listening on port " + to_string(config.m_webserver_listen_port) + ssl_option + "\n");
|
||||
webserver.init(&config, engine, outputs);
|
||||
webserver.start();
|
||||
// std::string ssl_option = (config.m_webserver_ssl_enabled ? " (SSL)" : "");
|
||||
// falco_logger::log(LOG_INFO, "Starting internal webserver, listening on port " + to_string(config.m_webserver_listen_port) + ssl_option + "\n");
|
||||
// webserver.init(&config, engine_future.get().get(), outputs);
|
||||
// webserver.start();
|
||||
}
|
||||
|
||||
// gRPC server
|
||||
@@ -1275,17 +1322,16 @@ int falco_init(int argc, char **argv)
|
||||
if(!trace_filename.empty() && !trace_is_scap)
|
||||
{
|
||||
#ifndef MINIMAL_BUILD
|
||||
read_k8s_audit_trace_file(engine,
|
||||
outputs,
|
||||
trace_filename);
|
||||
// read_k8s_audit_trace_file(engine.get(),
|
||||
// outputs,
|
||||
// trace_filename);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
uint64_t num_evts;
|
||||
|
||||
num_evts = do_inspect(engine,
|
||||
outputs,
|
||||
num_evts = do_inspect(bluengine, outputs,
|
||||
inspector,
|
||||
config,
|
||||
sdropmgr,
|
||||
@@ -1321,12 +1367,12 @@ int falco_init(int argc, char **argv)
|
||||
}
|
||||
|
||||
inspector->close();
|
||||
engine->print_stats();
|
||||
// engine->print_stats();
|
||||
sdropmgr.print_stats();
|
||||
if(engine_watchrules_thread.joinable())
|
||||
if(watchrules_thread.joinable())
|
||||
{
|
||||
hawk_destroy();
|
||||
engine_watchrules_thread.join();
|
||||
watchrules_thread.join();
|
||||
}
|
||||
#ifndef MINIMAL_BUILD
|
||||
webserver.stop();
|
||||
@@ -1343,10 +1389,10 @@ int falco_init(int argc, char **argv)
|
||||
|
||||
result = EXIT_FAILURE;
|
||||
|
||||
if(engine_watchrules_thread.joinable())
|
||||
if(watchrules_thread.joinable())
|
||||
{
|
||||
hawk_destroy();
|
||||
engine_watchrules_thread.join();
|
||||
watchrules_thread.join();
|
||||
}
|
||||
#ifndef MINIMAL_BUILD
|
||||
webserver.stop();
|
||||
@@ -1361,7 +1407,8 @@ int falco_init(int argc, char **argv)
|
||||
exit:
|
||||
|
||||
delete inspector;
|
||||
delete engine;
|
||||
delete bluengine;
|
||||
// delete engine.get();
|
||||
delete outputs;
|
||||
|
||||
return result;
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
extern void hawk_init();
|
||||
extern void hawk_destroy();
|
||||
|
||||
|
||||
typedef void* hawk_engine;
|
||||
typedef void (*hawk_watch_rules_cb)(char *rules_content, hawk_engine* engine);
|
||||
typedef void (*hawk_watch_rules_cb)(char* rules_content, hawk_engine* engine);
|
||||
extern void hawk_watch_rules(hawk_watch_rules_cb cb, hawk_engine* engine);
|
||||
|
||||
#endif //HAWK_H
|
||||
|
||||
Reference in New Issue
Block a user