Skip to content
API Documentation
BastionGuard™
Rule.cpp
Go to the documentation of this file.
1#include "bsc/rule/Rule.hpp"
2#include <algorithm>
3#include <functional>
4#include <array>
5#include <arpa/inet.h>
6#include <charconv>
7#include <chrono>
8#include <cctype>
9#include <filesystem>
10#include <fstream>
11#include <nlohmann/json.hpp>
12#include <ranges>
13#include <sstream>
14#include <unordered_set>
15#include <cstring>
16#include <openssl/evp.h>
17#include <pwd.h>
18
19namespace bsc::rule {
20using json = nlohmann::json;
21namespace fs = std::filesystem;
22namespace {
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();
27 return s;
28}
29
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);
33 if (!in) return {};
34 EVP_MD_CTX* ctx = EVP_MD_CTX_new();
35 if (!ctx) return {};
36 unsigned char md[EVP_MAX_MD_SIZE]{};
37 unsigned int md_len = 0;
38 std::array<char, 8192> buf{};
39 std::string out;
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]); }
46 }
47 }
48 EVP_MD_CTX_free(ctx);
49 return out;
50}
51
52std::string to_lower(std::string s) {
53 std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
54 return s;
55}
56std::string normalize_operand(std::string op) {
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;
78 return op;
79}
80std::vector<std::string> field_values(const bsc::model::Connection& c, const std::string& operand) {
81 const auto op = normalize_operand(operand);
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};
87 if (op == "process.path") return {c.process_path};
88 if (op == "process.name") return {c.process_comm};
89 if (op == "process.cwd") return {c.process_cwd};
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{}};
93 return {""};
94 }
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) {
101 if (i) oss << ' ';
102 oss << c.process_args[i];
103 }
104 return {oss.str()};
105 }
106 if (op == "process.parent.path") {
107 std::vector<std::string> out;
108 for (std::size_t i = 1; i < c.process_tree.size(); ++i) out.push_back(c.process_tree[i].first);
109 return out;
110 }
111 if (op.rfind("process.env.", 0) == 0) {
112 const auto key = op.substr(12);
113 if (const auto it = c.process_env.find(key); it != c.process_env.end()) return {it->second};
114 return {""};
115 }
116 if (op == "process.hash.sha256") {
117 if (const auto it = c.process_checksums.find("sha256"); it != c.process_checksums.end()) return {it->second};
118 return {""};
119 }
120 if (op == "process.hash.md5") {
121 if (const auto it = c.process_checksums.find("md5"); it != c.process_checksums.end()) return {it->second};
122 if (!c.process_path.empty()) return {digest_file(c.process_path, EVP_md5)};
123 return {""};
124 }
125 if (op == "process.hash.sha1") {
126 if (const auto it = c.process_checksums.find("sha1"); it != c.process_checksums.end()) return {it->second};
127 if (!c.process_path.empty()) return {digest_file(c.process_path, EVP_sha1)};
128 return {""};
129 }
130 if (op == "iface.in" || op == "iface.out") return {""};
131 return {""};
132}
133bool parse_ipv4(const std::string& ip, std::uint32_t& out) {
134 in_addr addr{};
135 if (inet_pton(AF_INET, ip.c_str(), &addr) != 1) return false;
136 out = ntohl(addr.s_addr);
137 return true;
138}
139bool parse_ipv6(const std::string& ip, in6_addr& out) {
140 return inet_pton(AF_INET6, ip.c_str(), &out) == 1;
141}
142bool ip_in_cidr(const std::string& ip, const std::string& cidr) {
143 const auto slash = cidr.find('/');
144 if (slash == std::string::npos) return to_lower(ip) == to_lower(cidr);
145 const auto net = cidr.substr(0, slash);
146 const auto bits = std::stoi(cidr.substr(slash + 1));
147 std::uint32_t ip4{}, net4{};
148 if (parse_ipv4(ip, ip4) && parse_ipv4(net, 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);
152 }
153 in6_addr ip6{}, net6{};
154 if (parse_ipv6(ip, ip6) && parse_ipv6(net, 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);
162 }
163 return false;
164}
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;
174 return {{min, max}};
175}
176std::unordered_set<std::string> load_simple_list_file(const fs::path& path, bool sensitive) {
177 std::unordered_set<std::string> out;
178 std::ifstream in(path);
179 std::string line;
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));
189 }
190 return out;
191}
193 Operator op;
194 const auto type = j.value("type", "unknown");
195 if (type == "simple") op.type = OperatorType::Simple;
196 else if (type == "regexp") op.type = OperatorType::Regexp;
197 else if (type == "network") op.type = OperatorType::Network;
198 else if (type == "range") op.type = OperatorType::Range;
199 else if (type == "list") op.type = OperatorType::List;
200 else if (type == "lists") op.type = OperatorType::Lists;
201 op.operand = normalize_operand(j.value("operand", ""));
202 op.data = j.value("data", "");
203 op.sensitive = j.value("sensitive", false);
204 if (j.contains("list") && j["list"].is_array()) {
205 for (const auto& item : j["list"]) op.list.push_back(parse_operator(item));
206 } else if (op.type == OperatorType::List && !op.data.empty() && op.data.front() == '[') {
207 try {
208 for (const auto& item : json::parse(op.data)) op.list.push_back(parse_operator(item));
209 } catch (...) {}
210 }
211 return op;
212}
214 std::string t = "unknown";
215 switch (op.type) {
216 case OperatorType::Simple: t = "simple"; break;
217 case OperatorType::Regexp: t = "regexp"; break;
218 case OperatorType::Network: t = "network"; break;
219 case OperatorType::Range: t = "range"; break;
220 case OperatorType::List: t = "list"; break;
221 case OperatorType::Lists: t = "lists"; break;
222 default: break;
223 }
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));
226 return j;
227}
228}
229
231 const auto op = normalize_operand(operand);
232 if (op == "true") return true;
233 if (type == OperatorType::List || op == "list") {
234 if (list.empty()) return true;
235 for (const auto& child : list) if (!child.matches(conn)) return false;
236 return true;
237 }
238 const auto values = field_values(conn, op);
239 auto str_matches = [&](const std::string& value) -> bool {
240 switch (type) {
242 return sensitive ? value == data : to_lower(value) == to_lower(data);
244 try {
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);
250 case OperatorType::Range: {
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;
256 }
257 case OperatorType::Lists: {
258 namespace fs = std::filesystem;
259 std::error_code ec;
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) {
264 std::ifstream in(p);
265 std::string line;
266 while (std::getline(in, line)) {
267 line = trim(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;
275 }
276 return false;
277 };
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;
280 return false;
281 }
282 default:
283 return false;
284 }
285 };
286 for (const auto& value : values) if (str_matches(value)) return true;
287 return false;
288}
289
290bool Rule::matches(const bsc::model::Connection& conn) const { return enabled && op.matches(conn); }
291
292void Loader::load_aliases(const std::string& path) {
293 aliases_.clear();
294 g_aliases.clear();
295 if (path.empty()) return;
296 std::ifstream in(path);
297 if (!in) return;
298 try {
299 json j; in >> j;
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>());
305 aliases_[it.key()] = vals;
306 g_aliases[it.key()] = std::move(vals);
307 }
308 } catch (...) {}
309}
310
311void Loader::load_directory(const std::string& dir) {
312 rules_.clear();
313 std::error_code ec;
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());
318 if (!in) continue;
319 json j; in >> j;
320 Rule r;
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());
324 }
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));
334 }
335 std::sort(rules_.begin(), rules_.end(), [](const Rule& a, const Rule& b) {
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;
339 });
340}
341
342bool Loader::save_rule(const Rule& rule, const std::string& dir) const {
343 std::error_code ec;
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';
355 return true;
356}
357
358std::optional<Rule> Loader::find_match(const bsc::model::Connection& conn) const {
359 for (const auto& r : rules_) if (r.matches(conn)) return r;
360 return std::nullopt;
361}
362
363} // namespace bsc::rule
#define AF_INET
Definition bsc_ebpf.c:56
#define AF_INET6
Definition bsc_ebpf.c:57
std::vector< Rule > rules_
Definition Rule.hpp:45
std::optional< Rule > find_match(const bsc::model::Connection &conn) const
Definition Rule.cpp:358
void load_aliases(const std::string &path)
Definition Rule.cpp:292
std::map< std::string, std::vector< std::string > > aliases_
Definition Rule.hpp:46
void load_directory(const std::string &dir)
Definition Rule.cpp:311
bool save_rule(const Rule &rule, const std::string &dir) const
Definition Rule.cpp:342
bool parse_ipv6(const std::string &ip, in6_addr &out)
Definition Rule.cpp:139
std::unordered_set< std::string > load_simple_list_file(const fs::path &path, bool sensitive)
Definition Rule.cpp:176
json to_json_op(const Operator &op)
Definition Rule.cpp:213
Operator parse_operator(const json &j)
Definition Rule.cpp:192
bool ip_in_cidr(const std::string &ip, const std::string &cidr)
Definition Rule.cpp:142
std::string trim(std::string s)
Definition Rule.cpp:24
std::optional< std::pair< unsigned long long, unsigned long long > > parse_range(const std::string &data)
Definition Rule.cpp:165
std::map< std::string, std::vector< std::string > > g_aliases
Definition Rule.cpp:23
std::string normalize_operand(std::string op)
Definition Rule.cpp:56
std::string to_lower(std::string s)
Definition Rule.cpp:52
std::vector< std::string > field_values(const bsc::model::Connection &c, const std::string &operand)
Definition Rule.cpp:80
std::string digest_file(const std::string &path, const EVP_MD *(*algo)())
Definition Rule.cpp:30
bool parse_ipv4(const std::string &ip, std::uint32_t &out)
Definition Rule.cpp:133
nlohmann::json json
Definition Rule.cpp:20
std::vector< std::pair< std::string, std::uint32_t > > process_tree
std::uint32_t process_id
std::vector< std::string > process_args
std::uint32_t dst_port
std::uint32_t src_port
std::map< std::string, std::string > process_checksums
std::map< std::string, std::string > process_env
std::uint32_t user_id
bool matches(const bsc::model::Connection &conn) const
Definition Rule.cpp:230
std::string operand
Definition Rule.hpp:15
std::string data
Definition Rule.hpp:16
OperatorType type
Definition Rule.hpp:14
std::vector< Operator > list
Definition Rule.hpp:18
Operator op
Definition Rule.hpp:32
bool matches(const bsc::model::Connection &conn) const
Definition Rule.cpp:290