11#include <nlohmann/json.hpp>
14#include <unordered_set>
16#include <openssl/evp.h>
20using json = nlohmann::json;
21namespace fs = std::filesystem;
23std::map<std::string, std::vector<std::string>>
g_aliases;
24std::string
trim(std::string s) {
25 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.front()))) s.erase(s.begin());
26 while (!s.empty() && std::isspace(
static_cast<unsigned char>(s.back()))) s.pop_back();
30std::string
digest_file(
const std::string& path,
const EVP_MD*(*algo)()) {
31 if (path.empty())
return {};
32 std::ifstream in(path, std::ios::binary);
34 EVP_MD_CTX* ctx = EVP_MD_CTX_new();
36 unsigned char md[EVP_MAX_MD_SIZE]{};
37 unsigned int md_len = 0;
38 std::array<char, 8192> buf{};
40 if (EVP_DigestInit_ex(ctx, algo(),
nullptr) == 1) {
41 while (in.read(buf.data(), buf.size()) || in.gcount() > 0) EVP_DigestUpdate(ctx, buf.data(),
static_cast<size_t>(in.gcount()));
42 if (EVP_DigestFinal_ex(ctx, md, &md_len) == 1) {
43 static const char* hex =
"0123456789abcdef";
44 out.reserve(md_len * 2);
45 for (
unsigned int i = 0; i < md_len; ++i) { out.push_back(hex[md[i] >> 4]); out.push_back(hex[md[i] & 0x0f]); }
53 std::transform(s.begin(), s.end(), s.begin(), [](
unsigned char c) { return static_cast<char>(std::tolower(c)); });
57 std::replace(op.begin(), op.end(),
'_',
'.');
58 if (op ==
"process.command" || op ==
"process.cmd")
return "process.command";
59 if (op ==
"process.hash.md5")
return op;
60 if (op ==
"process.hash.sha1")
return op;
61 if (op ==
"user.name")
return op;
62 if (op ==
"iface.in")
return op;
63 if (op ==
"iface.out")
return op;
64 if (op ==
"process.parent.path")
return op;
65 if (op ==
"process.name" || op ==
"process.comm")
return "process.name";
66 if (op ==
"process.path")
return op;
67 if (op ==
"process.cwd")
return op;
68 if (op ==
"process.id")
return op;
69 if (op ==
"protocol" || op ==
"true" || op ==
"list")
return op;
70 if (op ==
"source.ip" || op ==
"src.ip")
return "source.ip";
71 if (op ==
"source.port" || op ==
"src.port")
return "source.port";
72 if (op ==
"source.network" || op ==
"src.network")
return "source.network";
73 if (op ==
"dest.ip" || op ==
"dst.ip")
return "dest.ip";
74 if (op ==
"dest.host" || op ==
"dst.host")
return "dest.host";
75 if (op ==
"dest.port" || op ==
"dst.port")
return "dest.port";
76 if (op ==
"dest.network" || op ==
"dst.network")
return "dest.network";
77 if (op ==
"user.id")
return op;
82 if (op ==
"true")
return {
"true"};
83 if (op ==
"protocol")
return {c.
protocol};
84 if (op ==
"source.ip")
return {c.
src_ip};
85 if (op ==
"dest.ip")
return {c.
dst_ip};
86 if (op ==
"dest.host")
return {c.
dst_host};
90 if (op ==
"user.id")
return {std::to_string(c.
user_id)};
91 if (op ==
"user.name") {
92 if (passwd* pw = getpwuid(c.
user_id))
return {pw->pw_name ? std::string(pw->pw_name) : std::string{}};
95 if (op ==
"process.id")
return {std::to_string(c.
process_id)};
96 if (op ==
"source.port")
return {std::to_string(c.
src_port)};
97 if (op ==
"dest.port")
return {std::to_string(c.
dst_port)};
98 if (op ==
"process.command") {
99 std::ostringstream oss;
100 for (std::size_t i = 0; i < c.
process_args.size(); ++i) {
106 if (op ==
"process.parent.path") {
107 std::vector<std::string> out;
111 if (op.rfind(
"process.env.", 0) == 0) {
112 const auto key = op.substr(12);
116 if (op ==
"process.hash.sha256") {
120 if (op ==
"process.hash.md5") {
125 if (op ==
"process.hash.sha1") {
130 if (op ==
"iface.in" || op ==
"iface.out")
return {
""};
135 if (inet_pton(
AF_INET, ip.c_str(), &addr) != 1)
return false;
136 out = ntohl(addr.s_addr);
140 return inet_pton(
AF_INET6, ip.c_str(), &out) == 1;
142bool ip_in_cidr(
const std::string& ip,
const std::string& cidr) {
143 const auto slash = cidr.find(
'/');
145 const auto net = cidr.substr(0, slash);
146 const auto bits = std::stoi(cidr.substr(slash + 1));
147 std::uint32_t ip4{}, net4{};
149 if (bits < 0 || bits > 32)
return false;
150 const std::uint32_t mask = bits == 0 ? 0u : 0xFFFFFFFFu << (32 - bits);
151 return (ip4 & mask) == (net4 & mask);
153 in6_addr ip6{}, net6{};
155 if (bits < 0 || bits > 128)
return false;
156 const int full_bytes = bits / 8;
157 const int extra_bits = bits % 8;
158 if (full_bytes && std::memcmp(ip6.s6_addr, net6.s6_addr, full_bytes) != 0)
return false;
159 if (extra_bits == 0)
return true;
160 const std::uint8_t mask =
static_cast<std::uint8_t
>(0xFF << (8 - extra_bits));
161 return (ip6.s6_addr[full_bytes] & mask) == (net6.s6_addr[full_bytes] & mask);
165std::optional<std::pair<unsigned long long, unsigned long long>>
parse_range(
const std::string& data) {
166 const auto pos = data.find(
'-');
167 if (pos == std::string::npos)
return std::nullopt;
168 const auto a = data.substr(0, pos);
169 const auto b = data.substr(pos + 1);
170 unsigned long long min = 0, max = 0;
171 auto [p1, ec1] = std::from_chars(a.data(), a.data() + a.size(), min);
172 auto [p2, ec2] = std::from_chars(b.data(), b.data() + b.size(), max);
173 if (ec1 != std::errc{} || ec2 != std::errc{} || min > max)
return std::nullopt;
177 std::unordered_set<std::string> out;
178 std::ifstream in(path);
180 while (std::getline(in, line)) {
181 if (
const auto comment = line.find(
'#'); comment != std::string::npos) line.resize(comment);
182 line.erase(std::remove_if(line.begin(), line.end(), [](
unsigned char c) { return c ==
'\r' || c ==
'\n'; }), line.end());
183 while (!line.empty() && std::isspace(
static_cast<unsigned char>(line.back()))) line.pop_back();
184 std::size_t start = 0;
while (start < line.size() && std::isspace(
static_cast<unsigned char>(line[start]))) ++start;
185 if (start >= line.size())
continue;
186 auto item = line.substr(start);
187 if (!sensitive) item =
to_lower(item);
188 out.insert(std::move(item));
194 const auto type = j.value(
"type",
"unknown");
202 op.
data = j.value(
"data",
"");
203 op.
sensitive = j.value(
"sensitive",
false);
204 if (j.contains(
"list") && j[
"list"].is_array()) {
214 std::string t =
"unknown";
224 json j{{
"type", t}, {
"operand", op.
operand}, {
"data", op.
data}, {
"sensitive", op.
sensitive}, {
"list", json::array()}};
225 for (
const auto& child : op.
list) j[
"list"].push_back(
to_json_op(child));
231 const auto op = normalize_operand(
operand);
232 if (op ==
"true")
return true;
234 if (
list.empty())
return true;
235 for (
const auto& child :
list)
if (!child.matches(conn))
return false;
238 const auto values = field_values(conn, op);
239 auto str_matches = [&](
const std::string& value) ->
bool {
245 return std::regex_search(value, std::regex(
data,
sensitive ? std::regex::ECMAScript : (std::regex::ECMAScript | std::regex::icase)));
246 }
catch (...) {
return false; }
248 if (
const auto it = g_aliases.find(
data); it != g_aliases.end()) {
for (
const auto& cidr : it->second)
if (ip_in_cidr(value, cidr))
return true;
return false; }
249 return ip_in_cidr(value,
data);
251 const auto range = parse_range(
data);
252 if (!range)
return false;
253 unsigned long long n = 0;
254 auto [p, ec] = std::from_chars(value.data(), value.data() + value.size(), n);
255 return ec == std::errc{} && n >= range->first && n <= range->second;
258 namespace fs = std::filesystem;
260 if (!fs::exists(
data, ec))
return false;
261 const auto is_domain_regex = op ==
"lists.domains_regexp";
262 const auto is_nets = op ==
"lists.nets";
263 auto match_file = [&](
const fs::path& p) {
266 while (std::getline(in, line)) {
268 if (line.empty() || line[0] ==
'#')
continue;
269 if (is_nets) {
if (ip_in_cidr(value, line))
return true;
continue; }
270 if (is_domain_regex) {
try {
if (std::regex_search(value, std::regex(line,
sensitive ? std::regex::ECMAScript : (std::regex::ECMAScript | std::regex::icase))))
return true; }
catch (...) {}
continue; }
271 auto cmp =
sensitive ? line : to_lower(line);
272 auto vv =
sensitive ? value : to_lower(value);
273 if (op ==
"lists.domains" && vv.size() >= cmp.size()) {
if (vv == cmp || (vv.size() > cmp.size() && vv.ends_with(
'.' + cmp)))
return true; }
274 else if (cmp == vv)
return true;
278 if (fs::is_regular_file(
data, ec))
return match_file(
data);
279 for (
const auto& entry : fs::directory_iterator(
data, ec))
if (entry.is_regular_file() && match_file(entry.path()))
return true;
286 for (
const auto& value : values)
if (str_matches(value))
return true;
295 if (path.empty())
return;
296 std::ifstream in(path);
300 if (!j.is_object())
return;
301 for (
auto it = j.begin(); it != j.end(); ++it) {
302 if (!it.value().is_array())
continue;
303 std::vector<std::string> vals;
304 for (
const auto& item : it.value())
if (item.is_string()) vals.push_back(item.get<std::string>());
306 g_aliases[it.key()] = std::move(vals);
314 if (!fs::exists(dir, ec))
return;
315 for (
const auto& entry : fs::directory_iterator(dir, ec)) {
316 if (!entry.is_regular_file() || entry.path().extension() !=
".json")
continue;
317 std::ifstream in(entry.path());
321 if (j.contains(
"created")) {
322 if (j[
"created"].is_number_integer()) r.created = j[
"created"].get<std::int64_t>();
323 else r.created =
static_cast<std::int64_t
>(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::file_clock::to_sys(fs::last_write_time(entry.path())).time_since_epoch()).count());
325 r.name = j.value(
"name", entry.path().stem().string());
326 r.description = j.value(
"description",
"");
327 r.enabled = j.value(
"enabled",
true);
328 r.precedence = j.value(
"precedence",
false);
329 r.nolog = j.value(
"nolog",
false);
330 r.action = j.value(
"action",
"allow");
331 r.duration = j.value(
"duration",
"once");
332 if (j.contains(
"operator")) r.op = parse_operator(j[
"operator"]);
333 rules_.push_back(std::move(r));
336 if (a.precedence != b.precedence) return a.precedence > b.precedence;
337 if (a.created != b.created) return a.created < b.created;
338 return a.name < b.name;
344 fs::create_directories(dir, ec);
345 if (ec)
return false;
346 auto safe_name =
rule.name;
347 for (
char& c : safe_name)
if (!(std::isalnum(
static_cast<unsigned char>(c)) || c ==
'-' || c ==
'_' || c ==
'.')) c =
'_';
348 const auto path = fs::path(dir) / (safe_name +
".json");
349 json j{{
"created",
rule.created}, {
"name",
rule.name}, {
"description",
rule.description}, {
"enabled",
rule.enabled},
350 {
"precedence",
rule.precedence}, {
"nolog",
rule.nolog}, {
"action",
rule.action}, {
"duration",
rule.duration},
351 {
"operator", to_json_op(
rule.op)}};
352 std::ofstream out(path);
353 if (!out)
return false;
354 out << j.dump(2) <<
'\n';
359 for (
const auto& r :
rules_)
if (r.matches(conn))
return r;
std::vector< Rule > rules_
std::optional< Rule > find_match(const bsc::model::Connection &conn) const
void load_aliases(const std::string &path)
std::map< std::string, std::vector< std::string > > aliases_
void load_directory(const std::string &dir)
bool save_rule(const Rule &rule, const std::string &dir) const
bool parse_ipv6(const std::string &ip, in6_addr &out)
std::unordered_set< std::string > load_simple_list_file(const fs::path &path, bool sensitive)
json to_json_op(const Operator &op)
Operator parse_operator(const json &j)
bool ip_in_cidr(const std::string &ip, const std::string &cidr)
std::string trim(std::string s)
std::optional< std::pair< unsigned long long, unsigned long long > > parse_range(const std::string &data)
std::map< std::string, std::vector< std::string > > g_aliases
std::string normalize_operand(std::string op)
std::string to_lower(std::string s)
std::vector< std::string > field_values(const bsc::model::Connection &c, const std::string &operand)
std::string digest_file(const std::string &path, const EVP_MD *(*algo)())
bool parse_ipv4(const std::string &ip, std::uint32_t &out)
std::vector< std::pair< std::string, std::uint32_t > > process_tree
std::vector< std::string > process_args
std::map< std::string, std::string > process_checksums
std::map< std::string, std::string > process_env
bool matches(const bsc::model::Connection &conn) const
std::vector< Operator > list
bool matches(const bsc::model::Connection &conn) const