PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
atna_config.cpp
Go to the documentation of this file.
1
7
8#include <algorithm>
9#include <filesystem>
10#include <sstream>
11
12namespace kcenon::pacs::security {
13
14namespace {
15
16// =============================================================================
17// JSON Helpers (self-contained to avoid web module dependency)
18// =============================================================================
19
20std::string escape_json(std::string_view s) {
21 std::string result;
22 result.reserve(s.size() + 10);
23 for (char c : s) {
24 switch (c) {
25 case '"': result += "\\\""; break;
26 case '\\': result += "\\\\"; break;
27 case '\n': result += "\\n"; break;
28 case '\r': result += "\\r"; break;
29 case '\t': result += "\\t"; break;
30 default: result += c; break;
31 }
32 }
33 return result;
34}
35
37size_t skip_ws(std::string_view s, size_t pos) {
38 while (pos < s.size() && (s[pos] == ' ' || s[pos] == '\t' ||
39 s[pos] == '\n' || s[pos] == '\r')) {
40 ++pos;
41 }
42 return pos;
43}
44
46std::string extract_string(std::string_view s, size_t& pos) {
47 std::string result;
48 while (pos < s.size() && s[pos] != '"') {
49 if (s[pos] == '\\' && pos + 1 < s.size()) {
50 ++pos;
51 switch (s[pos]) {
52 case '"': result += '"'; break;
53 case '\\': result += '\\'; break;
54 case 'n': result += '\n'; break;
55 case 'r': result += '\r'; break;
56 case 't': result += '\t'; break;
57 default: result += s[pos]; break;
58 }
59 } else {
60 result += s[pos];
61 }
62 ++pos;
63 }
64 if (pos < s.size()) ++pos; // skip closing quote
65 return result;
66}
67
70std::string find_json_value(std::string_view json, std::string_view key) {
71 // Search for "key":
72 std::string needle = "\"" + std::string(key) + "\"";
73 auto key_pos = json.find(needle);
74 if (key_pos == std::string_view::npos) return "";
75
76 size_t pos = key_pos + needle.size();
77 pos = skip_ws(json, pos);
78 if (pos >= json.size() || json[pos] != ':') return "";
79 ++pos;
80 pos = skip_ws(json, pos);
81 if (pos >= json.size()) return "";
82
83 if (json[pos] == '"') {
84 // String value
85 ++pos;
86 return extract_string(json, pos);
87 }
88
89 // Non-string value (number, boolean, null)
90 size_t start = pos;
91 while (pos < json.size() && json[pos] != ',' && json[pos] != '}' &&
92 json[pos] != ' ' && json[pos] != '\n' && json[pos] != '\r') {
93 ++pos;
94 }
95 return std::string(json.substr(start, pos - start));
96}
97
99std::string find_nested_value(std::string_view json,
100 std::string_view section,
101 std::string_view key) {
102 // Find the section object
103 std::string needle = "\"" + std::string(section) + "\"";
104 auto sec_pos = json.find(needle);
105 if (sec_pos == std::string_view::npos) return "";
106
107 // Find the opening brace of the nested object
108 auto brace_pos = json.find('{', sec_pos + needle.size());
109 if (brace_pos == std::string_view::npos) return "";
110
111 // Find the matching closing brace
112 int depth = 1;
113 size_t end_pos = brace_pos + 1;
114 while (end_pos < json.size() && depth > 0) {
115 if (json[end_pos] == '{') ++depth;
116 else if (json[end_pos] == '}') --depth;
117 ++end_pos;
118 }
119
120 auto sub_json = json.substr(brace_pos, end_pos - brace_pos);
121 return find_json_value(sub_json, key);
122}
123
124bool parse_bool(const std::string& val, bool default_val) {
125 if (val == "true") return true;
126 if (val == "false") return false;
127 return default_val;
128}
129
130uint16_t parse_uint16(const std::string& val, uint16_t default_val) {
131 if (val.empty()) return default_val;
132 try {
133 auto v = std::stoul(val);
134 if (v > 65535) return default_val;
135 return static_cast<uint16_t>(v);
136 } catch (...) {
137 return default_val;
138 }
139}
140
141syslog_transport_protocol parse_protocol(const std::string& val) {
142 if (val == "tls") return syslog_transport_protocol::tls;
144}
145
146std::string protocol_to_string(syslog_transport_protocol p) {
147 return (p == syslog_transport_protocol::tls) ? "tls" : "udp";
148}
149
150} // namespace
151
152// =============================================================================
153// Default Configuration
154// =============================================================================
155
159
160// =============================================================================
161// JSON Serialization
162// =============================================================================
163
164std::string to_json(const atna_config& config) {
165 std::ostringstream oss;
166 oss << R"({"enabled":)" << (config.enabled ? "true" : "false")
167 << R"(,"audit_source_id":")" << escape_json(config.audit_source_id)
168 << R"(","transport":{"protocol":")"
169 << protocol_to_string(config.transport.protocol)
170 << R"(","host":")" << escape_json(config.transport.host)
171 << R"(","port":)" << config.transport.port
172 << R"(,"app_name":")" << escape_json(config.transport.app_name)
173 << R"(","ca_cert_path":")" << escape_json(config.transport.ca_cert_path)
174 << R"(","client_cert_path":")"
175 << escape_json(config.transport.client_cert_path)
176 << R"(","client_key_path":")"
177 << escape_json(config.transport.client_key_path)
178 << R"(","verify_server":)"
179 << (config.transport.verify_server ? "true" : "false")
180 << R"(},"audit_storage":)"
181 << (config.audit_storage ? "true" : "false")
182 << R"(,"audit_query":)"
183 << (config.audit_query ? "true" : "false")
184 << R"(,"audit_authentication":)"
185 << (config.audit_authentication ? "true" : "false")
186 << R"(,"audit_security_alerts":)"
187 << (config.audit_security_alerts ? "true" : "false")
188 << "}";
189 return oss.str();
190}
191
192// =============================================================================
193// JSON Parsing
194// =============================================================================
195
196atna_config parse_atna_config(std::string_view json_str) {
197 atna_config config;
198
199 // Top-level fields
200 auto enabled_val = find_json_value(json_str, "enabled");
201 if (!enabled_val.empty()) {
202 config.enabled = parse_bool(enabled_val, config.enabled);
203 }
204
205 auto source_id = find_json_value(json_str, "audit_source_id");
206 if (!source_id.empty()) {
207 config.audit_source_id = source_id;
208 }
209
210 // Event filtering
211 auto storage_val = find_json_value(json_str, "audit_storage");
212 if (!storage_val.empty()) {
213 config.audit_storage = parse_bool(storage_val, config.audit_storage);
214 }
215
216 auto query_val = find_json_value(json_str, "audit_query");
217 if (!query_val.empty()) {
218 config.audit_query = parse_bool(query_val, config.audit_query);
219 }
220
221 auto auth_val = find_json_value(json_str, "audit_authentication");
222 if (!auth_val.empty()) {
223 config.audit_authentication =
224 parse_bool(auth_val, config.audit_authentication);
225 }
226
227 auto alerts_val = find_json_value(json_str, "audit_security_alerts");
228 if (!alerts_val.empty()) {
229 config.audit_security_alerts =
230 parse_bool(alerts_val, config.audit_security_alerts);
231 }
232
233 // Transport nested object
234 auto proto = find_nested_value(json_str, "transport", "protocol");
235 if (!proto.empty()) {
236 config.transport.protocol = parse_protocol(proto);
237 }
238
239 auto host = find_nested_value(json_str, "transport", "host");
240 if (!host.empty()) {
241 config.transport.host = host;
242 }
243
244 auto port = find_nested_value(json_str, "transport", "port");
245 if (!port.empty()) {
246 config.transport.port =
247 parse_uint16(port, config.transport.port);
248 }
249
250 auto app = find_nested_value(json_str, "transport", "app_name");
251 if (!app.empty()) {
252 config.transport.app_name = app;
253 }
254
255 auto ca = find_nested_value(json_str, "transport", "ca_cert_path");
256 if (!ca.empty()) {
257 config.transport.ca_cert_path = ca;
258 }
259
260 auto cert = find_nested_value(json_str, "transport", "client_cert_path");
261 if (!cert.empty()) {
262 config.transport.client_cert_path = cert;
263 }
264
265 auto key = find_nested_value(json_str, "transport", "client_key_path");
266 if (!key.empty()) {
267 config.transport.client_key_path = key;
268 }
269
270 auto verify = find_nested_value(json_str, "transport", "verify_server");
271 if (!verify.empty()) {
272 config.transport.verify_server =
273 parse_bool(verify, config.transport.verify_server);
274 }
275
276 return config;
277}
278
279// =============================================================================
280// Validation
281// =============================================================================
282
285
286 if (config.audit_source_id.empty()) {
287 result.valid = false;
288 result.errors.emplace_back("audit_source_id must not be empty");
289 }
290
291 if (config.transport.host.empty()) {
292 result.valid = false;
293 result.errors.emplace_back("transport.host must not be empty");
294 }
295
296 if (config.transport.port == 0) {
297 result.valid = false;
298 result.errors.emplace_back(
299 "transport.port must be in range 1-65535");
300 }
301
303 if (config.transport.ca_cert_path.empty()) {
304 result.valid = false;
305 result.errors.emplace_back(
306 "transport.ca_cert_path is required for TLS protocol");
307 } else if (!std::filesystem::exists(
308 config.transport.ca_cert_path)) {
309 result.valid = false;
310 result.errors.emplace_back(
311 "transport.ca_cert_path file does not exist: " +
312 config.transport.ca_cert_path);
313 }
314
315 if (!config.transport.client_cert_path.empty() &&
316 !std::filesystem::exists(config.transport.client_cert_path)) {
317 result.valid = false;
318 result.errors.emplace_back(
319 "transport.client_cert_path file does not exist: " +
321 }
322
323 if (!config.transport.client_key_path.empty() &&
324 !std::filesystem::exists(config.transport.client_key_path)) {
325 result.valid = false;
326 result.errors.emplace_back(
327 "transport.client_key_path file does not exist: " +
329 }
330 }
331
332 return result;
333}
334
335} // namespace kcenon::pacs::security
Configuration management for ATNA audit logging.
std::optional< std::string > extract_string(const std::string &json, const std::string &key)
Simple JSON value extractor (for basic parsing)
syslog_transport_protocol
Syslog transport protocol.
@ udp
UDP (RFC 5426) — Fire-and-forget.
@ tls
TLS over TCP (RFC 5425) — Secure.
std::string to_json(const atna_config &config)
Serialize an atna_config to a JSON string.
atna_config make_default_atna_config()
Create a default ATNA configuration.
atna_config parse_atna_config(std::string_view json_str)
Parse an atna_config from a JSON string.
atna_config_validation validate(const atna_config &config)
Validate an ATNA configuration.
Validation result for ATNA configuration.
Configuration for ATNA audit logging.
Definition atna_config.h:52
bool audit_security_alerts
Audit security alert events (access denied, etc.)
Definition atna_config.h:74
syslog_transport_config transport
Syslog transport configuration.
Definition atna_config.h:60
bool enabled
Master enable/disable for ATNA audit logging.
Definition atna_config.h:54
bool audit_storage
Audit C-STORE events (DICOM Instances Transferred)
Definition atna_config.h:65
bool audit_query
Audit C-FIND events (Query)
Definition atna_config.h:68
bool audit_authentication
Audit login/logout events (User Authentication)
Definition atna_config.h:71
std::string audit_source_id
Audit source identifier (e.g., "PACS_SYSTEM_01")
Definition atna_config.h:57
std::string client_key_path
Path to client private key file (mutual TLS)
std::string ca_cert_path
Path to CA certificate file for server verification.
syslog_transport_protocol protocol
Transport protocol (UDP or TLS)
uint16_t port
Port number (514 for UDP, 6514 for TLS per IANA)
std::string host
Audit Record Repository hostname or IP.
std::string app_name
Application name in Syslog header.
std::string client_cert_path
Path to client certificate file (mutual TLS)
bool verify_server
Whether to verify server certificate (disable only for testing)