PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
atna_audit_logger.cpp
Go to the documentation of this file.
1
7
8#include <ctime>
9#include <iomanip>
10#include <sstream>
11
12namespace kcenon::pacs::security {
13
14// =============================================================================
15// XML Generation
16// =============================================================================
17
19 std::ostringstream xml;
20
21 xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
22 xml << "<AuditMessage>\n";
23
24 // -- EventIdentification --
25 xml << " <EventIdentification"
26 << " EventActionCode=\""
27 << static_cast<char>(msg.event_action) << "\""
28 << " EventDateTime=\"" << format_datetime(msg.event_date_time) << "\""
29 << " EventOutcomeIndicator=\""
30 << static_cast<int>(msg.event_outcome) << "\">\n";
31
32 xml << " <EventID " << format_coded_value_attrs(msg.event_id) << "/>\n";
33
34 for (const auto& etc : msg.event_type_codes) {
35 xml << " <EventTypeCode " << format_coded_value_attrs(etc)
36 << "/>\n";
37 }
38
39 xml << " </EventIdentification>\n";
40
41 // -- ActiveParticipant(s) --
42 for (const auto& ap : msg.active_participants) {
43 xml << " <ActiveParticipant"
44 << " UserID=\"" << xml_escape(ap.user_id) << "\"";
45
46 if (!ap.alternative_user_id.empty()) {
47 xml << " AlternativeUserID=\""
48 << xml_escape(ap.alternative_user_id) << "\"";
49 }
50
51 if (!ap.user_name.empty()) {
52 xml << " UserName=\"" << xml_escape(ap.user_name) << "\"";
53 }
54
55 xml << " UserIsRequestor=\""
56 << (ap.user_is_requestor ? "true" : "false") << "\"";
57
58 if (!ap.network_access_point_id.empty()) {
59 xml << " NetworkAccessPointID=\""
60 << xml_escape(ap.network_access_point_id) << "\""
61 << " NetworkAccessPointTypeCode=\""
62 << static_cast<int>(ap.network_access_point_type) << "\"";
63 }
64
65 if (ap.role_id_codes.empty()) {
66 xml << "/>\n";
67 } else {
68 xml << ">\n";
69 for (const auto& role : ap.role_id_codes) {
70 xml << " <RoleIDCode "
71 << format_coded_value_attrs(role) << "/>\n";
72 }
73 xml << " </ActiveParticipant>\n";
74 }
75 }
76
77 // -- AuditSourceIdentification --
78 xml << " <AuditSourceIdentification"
79 << " AuditSourceID=\""
81
82 if (!msg.audit_source.audit_enterprise_site_id.empty()) {
83 xml << " AuditEnterpriseSiteID=\""
85 }
86
87 if (msg.audit_source.audit_source_type_codes.empty()) {
88 xml << "/>\n";
89 } else {
90 xml << ">\n";
91 for (auto type_code : msg.audit_source.audit_source_type_codes) {
92 xml << " <AuditSourceTypeCode code=\""
93 << static_cast<int>(type_code) << "\"/>\n";
94 }
95 xml << " </AuditSourceIdentification>\n";
96 }
97
98 // -- ParticipantObjectIdentification(s) --
99 for (const auto& po : msg.participant_objects) {
100 xml << " <ParticipantObjectIdentification"
101 << " ParticipantObjectTypeCode=\""
102 << static_cast<int>(po.object_type) << "\""
103 << " ParticipantObjectTypeCodeRole=\""
104 << static_cast<int>(po.object_role) << "\"";
105
106 if (!po.object_id.empty()) {
107 xml << " ParticipantObjectID=\""
108 << xml_escape(po.object_id) << "\"";
109 }
110
111 if (!po.object_name.empty()) {
112 xml << " ParticipantObjectName=\""
113 << xml_escape(po.object_name) << "\"";
114 }
115
116 bool has_children = !po.object_id_type_code.code.empty() ||
117 !po.object_query.empty() ||
118 !po.object_details.empty();
119
120 if (!has_children) {
121 xml << "/>\n";
122 } else {
123 xml << ">\n";
124
125 if (!po.object_id_type_code.code.empty()) {
126 xml << " <ParticipantObjectIDTypeCode "
127 << format_coded_value_attrs(po.object_id_type_code)
128 << "/>\n";
129 }
130
131 if (!po.object_query.empty()) {
132 xml << " <ParticipantObjectQuery>"
133 << xml_escape(po.object_query)
134 << "</ParticipantObjectQuery>\n";
135 }
136
137 for (const auto& detail : po.object_details) {
138 xml << " <ParticipantObjectDetail"
139 << " type=\"" << xml_escape(detail.type) << "\""
140 << " value=\"" << xml_escape(detail.value) << "\"/>\n";
141 }
142
143 xml << " </ParticipantObjectIdentification>\n";
144 }
145 }
146
147 xml << "</AuditMessage>\n";
148
149 return xml.str();
150}
151
152// =============================================================================
153// Event Factory Methods
154// =============================================================================
155
157 const std::string& source_id,
158 const std::string& app_name,
159 bool is_start,
160 atna_event_outcome outcome) {
161
164 msg.event_type_codes.push_back(
168 msg.event_date_time = std::chrono::system_clock::now();
169 msg.event_outcome = outcome;
170
172 ap.user_id = app_name;
173 ap.user_is_requestor = false;
174 ap.role_id_codes.push_back(atna_role_ids::application);
175 msg.active_participants.push_back(std::move(ap));
176
177 msg.audit_source.audit_source_id = source_id;
178
179 return msg;
180}
181
183 const std::string& source_id,
184 const std::string& user_id,
185 const std::string& user_ip,
186 const std::string& study_uid,
187 const std::string& patient_id,
188 atna_event_outcome outcome) {
189
193 msg.event_date_time = std::chrono::system_clock::now();
194 msg.event_outcome = outcome;
195
197 ap.user_id = user_id;
198 ap.user_is_requestor = true;
199 ap.network_access_point_id = user_ip;
200 ap.network_access_point_type = atna_network_access_type::ip_address;
201 msg.active_participants.push_back(std::move(ap));
202
203 msg.audit_source.audit_source_id = source_id;
204
205 // Study object
206 if (!study_uid.empty()) {
211 obj.object_id = study_uid;
212 msg.participant_objects.push_back(std::move(obj));
213 }
214
215 // Patient object
216 if (!patient_id.empty()) {
217 atna_participant_object patient_obj;
221 patient_obj.object_id = patient_id;
222 msg.participant_objects.push_back(std::move(patient_obj));
223 }
224
225 return msg;
226}
227
229 const std::string& source_id,
230 const std::string& source_ae,
231 const std::string& source_ip,
232 const std::string& dest_ae,
233 const std::string& dest_ip,
234 const std::string& study_uid,
235 const std::string& patient_id,
236 bool is_import,
237 atna_event_outcome outcome) {
238
243 msg.event_date_time = std::chrono::system_clock::now();
244 msg.event_outcome = outcome;
245
246 // Source participant
248 src.user_id = source_ae;
249 src.user_is_requestor = !is_import;
250 src.network_access_point_id = source_ip;
253 msg.active_participants.push_back(std::move(src));
254
255 // Destination participant
257 dst.user_id = dest_ae;
258 dst.user_is_requestor = is_import;
259 dst.network_access_point_id = dest_ip;
262 msg.active_participants.push_back(std::move(dst));
263
264 msg.audit_source.audit_source_id = source_id;
265
266 // Study object
267 if (!study_uid.empty()) {
272 obj.object_id = study_uid;
273 msg.participant_objects.push_back(std::move(obj));
274 }
275
276 // Patient object
277 if (!patient_id.empty()) {
278 atna_participant_object patient_obj;
282 patient_obj.object_id = patient_id;
283 msg.participant_objects.push_back(std::move(patient_obj));
284 }
285
286 return msg;
287}
288
290 const std::string& source_id,
291 const std::string& user_id,
292 const std::string& user_ip,
293 const std::string& study_uid,
294 const std::string& patient_id,
295 atna_event_outcome outcome) {
296
300 msg.event_date_time = std::chrono::system_clock::now();
301 msg.event_outcome = outcome;
302
304 ap.user_id = user_id;
305 ap.user_is_requestor = true;
306 ap.network_access_point_id = user_ip;
307 ap.network_access_point_type = atna_network_access_type::ip_address;
308 msg.active_participants.push_back(std::move(ap));
309
310 msg.audit_source.audit_source_id = source_id;
311
312 // Study object
317 obj.object_id = study_uid;
318 msg.participant_objects.push_back(std::move(obj));
319
320 // Patient object
321 if (!patient_id.empty()) {
322 atna_participant_object patient_obj;
326 patient_obj.object_id = patient_id;
327 msg.participant_objects.push_back(std::move(patient_obj));
328 }
329
330 return msg;
331}
332
334 const std::string& source_id,
335 const std::string& user_id,
336 const std::string& user_ip,
337 const std::string& alert_description,
338 atna_event_outcome outcome) {
339
343 msg.event_date_time = std::chrono::system_clock::now();
344 msg.event_outcome = outcome;
345
347 ap.user_id = user_id;
348 ap.user_is_requestor = true;
349 ap.network_access_point_id = user_ip;
350 ap.network_access_point_type = atna_network_access_type::ip_address;
351 msg.active_participants.push_back(std::move(ap));
352
353 msg.audit_source.audit_source_id = source_id;
354
355 // Alert description as participant object
359 obj.object_id = source_id;
360 obj.object_name = alert_description;
361 msg.participant_objects.push_back(std::move(obj));
362
363 return msg;
364}
365
367 const std::string& source_id,
368 const std::string& user_id,
369 const std::string& user_ip,
370 bool is_login,
371 atna_event_outcome outcome) {
372
375 msg.event_type_codes.push_back(
378 msg.event_date_time = std::chrono::system_clock::now();
379 msg.event_outcome = outcome;
380
382 ap.user_id = user_id;
383 ap.user_is_requestor = true;
384 ap.network_access_point_id = user_ip;
385 ap.network_access_point_type = atna_network_access_type::ip_address;
386 msg.active_participants.push_back(std::move(ap));
387
388 msg.audit_source.audit_source_id = source_id;
389
390 return msg;
391}
392
394 const std::string& source_id,
395 const std::string& user_id,
396 const std::string& user_ip,
397 const std::string& query_data,
398 const std::string& patient_id,
399 atna_event_outcome outcome) {
400
404 msg.event_date_time = std::chrono::system_clock::now();
405 msg.event_outcome = outcome;
406
408 ap.user_id = user_id;
409 ap.user_is_requestor = true;
410 ap.network_access_point_id = user_ip;
411 ap.network_access_point_type = atna_network_access_type::ip_address;
412 ap.role_id_codes.push_back(atna_role_ids::source);
413 msg.active_participants.push_back(std::move(ap));
414
415 msg.audit_source.audit_source_id = source_id;
416
417 // Query object
418 atna_participant_object query_obj;
422 query_obj.object_query = query_data;
423 msg.participant_objects.push_back(std::move(query_obj));
424
425 // Patient object
426 if (!patient_id.empty()) {
427 atna_participant_object patient_obj;
431 patient_obj.object_id = patient_id;
432 msg.participant_objects.push_back(std::move(patient_obj));
433 }
434
435 return msg;
436}
437
439 const std::string& source_id,
440 const std::string& user_id,
441 const std::string& user_ip,
442 const std::string& dest_id,
443 const std::string& study_uid,
444 const std::string& patient_id,
445 atna_event_outcome outcome) {
446
450 msg.event_date_time = std::chrono::system_clock::now();
451 msg.event_outcome = outcome;
452
453 // Exporter
455 exporter.user_id = user_id;
456 exporter.user_is_requestor = true;
457 exporter.network_access_point_id = user_ip;
459 exporter.role_id_codes.push_back(atna_role_ids::source);
460 msg.active_participants.push_back(std::move(exporter));
461
462 // Destination
464 dest.user_id = dest_id;
465 dest.user_is_requestor = false;
467 msg.active_participants.push_back(std::move(dest));
468
469 msg.audit_source.audit_source_id = source_id;
470
471 // Study object
472 if (!study_uid.empty()) {
477 obj.object_id = study_uid;
478 msg.participant_objects.push_back(std::move(obj));
479 }
480
481 // Patient object
482 if (!patient_id.empty()) {
483 atna_participant_object patient_obj;
487 patient_obj.object_id = patient_id;
488 msg.participant_objects.push_back(std::move(patient_obj));
489 }
490
491 return msg;
492}
493
494// =============================================================================
495// Private XML Helpers
496// =============================================================================
497
498std::string atna_audit_logger::xml_escape(const std::string& str) {
499 std::string result;
500 result.reserve(str.size());
501
502 for (char c : str) {
503 switch (c) {
504 case '&': result += "&amp;"; break;
505 case '<': result += "&lt;"; break;
506 case '>': result += "&gt;"; break;
507 case '"': result += "&quot;"; break;
508 case '\'': result += "&apos;"; break;
509 default: result += c; break;
510 }
511 }
512
513 return result;
514}
515
517 std::chrono::system_clock::time_point tp) {
518
519 auto time_t_val = std::chrono::system_clock::to_time_t(tp);
520 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
521 tp.time_since_epoch()) % 1000;
522
523 std::tm tm_val{};
524#if defined(_WIN32)
525 gmtime_s(&tm_val, &time_t_val);
526#else
527 gmtime_r(&time_t_val, &tm_val);
528#endif
529
530 std::ostringstream oss;
531 oss << std::put_time(&tm_val, "%Y-%m-%dT%H:%M:%S")
532 << '.' << std::setfill('0') << std::setw(3) << ms.count()
533 << "Z";
534
535 return oss.str();
536}
537
539 const atna_coded_value& cv) {
540
541 std::ostringstream oss;
542 oss << "csd-code=\"" << xml_escape(cv.code) << "\"";
543
544 if (!cv.code_system_name.empty()) {
545 oss << " codeSystemName=\"" << xml_escape(cv.code_system_name) << "\"";
546 }
547
548 if (!cv.display_name.empty()) {
549 oss << " originalText=\"" << xml_escape(cv.display_name) << "\"";
550 }
551
552 return oss.str();
553}
554
555} // namespace kcenon::pacs::security
IHE ATNA-compliant audit message generator (RFC 3881 XML format)
static atna_audit_message build_dicom_instances_transferred(const std::string &source_id, const std::string &source_ae, const std::string &source_ip, const std::string &dest_ae, const std::string &dest_ip, const std::string &study_uid, const std::string &patient_id="", bool is_import=true, atna_event_outcome outcome=atna_event_outcome::success)
Build DICOM Instances Transferred audit message (C-STORE, C-MOVE)
static std::string format_coded_value_attrs(const atna_coded_value &cv)
static std::string to_xml(const atna_audit_message &message)
Serialize an audit message to RFC 3881 XML.
static atna_audit_message build_study_deleted(const std::string &source_id, const std::string &user_id, const std::string &user_ip, const std::string &study_uid, const std::string &patient_id="", atna_event_outcome outcome=atna_event_outcome::success)
Build Study Deleted audit message.
static std::string xml_escape(const std::string &str)
static atna_audit_message build_application_activity(const std::string &source_id, const std::string &app_name, bool is_start, atna_event_outcome outcome=atna_event_outcome::success)
Build Application Activity audit message (start/stop)
static atna_audit_message build_export(const std::string &source_id, const std::string &user_id, const std::string &user_ip, const std::string &dest_id, const std::string &study_uid, const std::string &patient_id="", atna_event_outcome outcome=atna_event_outcome::success)
Build Export audit message (media/network export)
static std::string format_datetime(std::chrono::system_clock::time_point tp)
static atna_audit_message build_security_alert(const std::string &source_id, const std::string &user_id, const std::string &user_ip, const std::string &alert_description, atna_event_outcome outcome=atna_event_outcome::serious_failure)
Build Security Alert audit message.
static atna_audit_message build_user_authentication(const std::string &source_id, const std::string &user_id, const std::string &user_ip, bool is_login, atna_event_outcome outcome=atna_event_outcome::success)
Build User Authentication audit message (login/logout)
static atna_audit_message build_query(const std::string &source_id, const std::string &user_id, const std::string &user_ip, const std::string &query_data, const std::string &patient_id="", atna_event_outcome outcome=atna_event_outcome::success)
Build Query audit message (C-FIND, QIDO-RS)
static atna_audit_message build_dicom_instances_accessed(const std::string &source_id, const std::string &user_id, const std::string &user_ip, const std::string &study_uid, const std::string &patient_id="", atna_event_outcome outcome=atna_event_outcome::success)
Build DICOM Instances Accessed audit message (C-FIND, QIDO-RS)
const atna_coded_value dicom_instances_accessed
DICOM Instances Accessed (110103)
const atna_coded_value query
Query (110112)
const atna_coded_value user_authentication
User Authentication (110114)
const atna_coded_value security_alert
Security Alert (110113)
const atna_coded_value dicom_instances_transferred
DICOM Instances Transferred (110104)
const atna_coded_value dicom_study_deleted
DICOM Study Deleted (110105)
const atna_coded_value data_export
Export (110106)
const atna_coded_value application_activity
Application Activity (110100) — Start/Stop.
const atna_coded_value application_start
Application Start.
const atna_coded_value application_stop
Application Stop.
const atna_coded_value sop_class_uid
SOP Class UID.
const atna_coded_value study_instance_uid
Study Instance UID.
const atna_coded_value patient_number
Patient Number (RFC 3881 defined)
const atna_coded_value application
Application (110150)
const atna_coded_value source
Source Role ID (110153)
const atna_coded_value destination
Destination Role ID (110152)
atna_event_outcome
Outcome of the audited event.
An active participant in the audit event.
std::string network_access_point_id
Network access point (hostname or IP)
std::string user_id
User or process identifier.
bool user_is_requestor
Whether this participant initiated the event.
std::vector< atna_coded_value > role_id_codes
Role(s) of this participant.
atna_network_access_type network_access_point_type
Type of network access point.
atna_event_action event_action
Action that triggered the event.
std::vector< atna_participant_object > participant_objects
Participant objects (patients/studies/queries)
std::vector< atna_coded_value > event_type_codes
Event type codes for sub-classification.
atna_event_outcome event_outcome
Outcome of the event.
atna_coded_value event_id
Event ID coded value (e.g., DCM 110114 = UserAuthentication)
atna_audit_source audit_source
Audit source identification.
std::vector< atna_active_participant > active_participants
Active participants (users/processes)
std::chrono::system_clock::time_point event_date_time
When the event occurred.
std::string audit_source_id
Unique identifier for the audit source.
std::vector< uint8_t > audit_source_type_codes
Audit source type codes (optional)
std::string audit_enterprise_site_id
Enterprise site identifier (optional)
A coded value with code, code system name, and display name.
An object (patient, study, query) involved in the event.
std::string object_id
Object identifier (e.g., Patient ID, Study UID)
std::string object_query
Query data (base64 encoded, for query events)
atna_object_role object_role
Role of the object in the event.
atna_coded_value object_id_type_code
Object ID type code.
std::string object_name
Human-readable object name.