1use crate::parser::{parse_inner_segments, parse_message, RawSegment};
7use chrono::{DateTime, Utc};
8use serde_json::{json, Value};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ViolationSeverity {
14 Error,
16 Warning,
18 Info,
20}
21
22impl ViolationSeverity {
23 fn as_str(&self) -> &'static str {
24 match self {
25 ViolationSeverity::Error => "ERROR",
26 ViolationSeverity::Warning => "WARNING",
27 ViolationSeverity::Info => "INFO",
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
33pub struct Violation {
34 pub severity: ViolationSeverity,
35 pub rule: String,
37 pub description: String,
39 pub segment: Option<String>,
41}
42
43pub struct AuditReport {
44 pub violations: Vec<Violation>,
45 pub segments_checked: usize,
46 pub timestamp: DateTime<Utc>,
47}
48
49impl AuditReport {
50 pub fn has_errors(&self) -> bool {
51 self.violations
52 .iter()
53 .any(|v| v.severity == ViolationSeverity::Error)
54 }
55
56 pub fn error_count(&self) -> usize {
57 self.violations
58 .iter()
59 .filter(|v| v.severity == ViolationSeverity::Error)
60 .count()
61 }
62
63 pub fn warning_count(&self) -> usize {
64 self.violations
65 .iter()
66 .filter(|v| v.severity == ViolationSeverity::Warning)
67 .count()
68 }
69
70 pub fn format_report(&self) -> String {
72 let mut out = String::new();
73 out.push_str(&format!(
74 "FinTS Audit Report — {} — {} segments checked\n",
75 self.timestamp.format("%Y-%m-%dT%H:%M:%SZ"),
76 self.segments_checked
77 ));
78 out.push_str(&format!(
79 " Errors: {} Warnings: {} Total violations: {}\n",
80 self.error_count(),
81 self.warning_count(),
82 self.violations.len()
83 ));
84
85 if self.violations.is_empty() {
86 out.push_str(" No violations found.\n");
87 } else {
88 out.push_str(" Violations:\n");
89 for v in &self.violations {
90 let seg_part = v
91 .segment
92 .as_deref()
93 .map(|s| format!(" [{}]", s))
94 .unwrap_or_default();
95 out.push_str(&format!(
96 " [{:7}] {:12}{} — {}\n",
97 v.severity.as_str(),
98 v.rule,
99 seg_part,
100 v.description
101 ));
102 }
103 }
104 out
105 }
106
107 pub fn to_json(&self) -> Value {
109 let violations: Vec<Value> = self
110 .violations
111 .iter()
112 .map(|v| {
113 json!({
114 "severity": v.severity.as_str(),
115 "rule": v.rule,
116 "description": v.description,
117 "segment": v.segment,
118 })
119 })
120 .collect();
121
122 json!({
123 "timestamp": self.timestamp.to_rfc3339(),
124 "segments_checked": self.segments_checked,
125 "error_count": self.error_count(),
126 "warning_count": self.warning_count(),
127 "violations": violations,
128 })
129 }
130}
131
132pub fn audit_client_message(data: &[u8]) -> AuditReport {
136 let mut ctx = AuditContext::new();
137
138 let segments = match parse_message(data) {
139 Ok(s) => s,
140 Err(e) => {
141 ctx.add(
142 ViolationSeverity::Error,
143 "PARSE-001",
144 &format!("Message could not be parsed: {}", e),
145 None,
146 );
147 return ctx.finish(0);
148 }
149 };
150
151 let count = segments.len();
152 check_hnhbk_rules(&segments, &mut ctx);
153 check_hnvsk_rules(&segments, &mut ctx);
154 check_hnvsd_rules(&segments, &mut ctx, true);
155 check_hnhbs_rule(&segments, &mut ctx);
156
157 ctx.finish(count)
158}
159
160pub fn audit_server_response(data: &[u8]) -> AuditReport {
162 let mut ctx = AuditContext::new();
163
164 let segments = match parse_message(data) {
165 Ok(s) => s,
166 Err(e) => {
167 ctx.add(
168 ViolationSeverity::Error,
169 "PARSE-001",
170 &format!("Message could not be parsed: {}", e),
171 None,
172 );
173 return ctx.finish(0);
174 }
175 };
176
177 let count = segments.len();
178 check_hnhbk_rules(&segments, &mut ctx);
179 check_hnvsk_rules(&segments, &mut ctx);
180 check_hnvsd_rules(&segments, &mut ctx, false);
181 check_hirmg_rules(&segments, &mut ctx);
182 check_response_code_rules(&segments, &mut ctx);
183 check_hnhbs_rule(&segments, &mut ctx);
184
185 ctx.finish(count)
186}
187
188struct AuditContext {
191 violations: Vec<Violation>,
192}
193
194impl AuditContext {
195 fn new() -> Self {
196 AuditContext {
197 violations: Vec::new(),
198 }
199 }
200
201 fn add(
202 &mut self,
203 severity: ViolationSeverity,
204 rule: &str,
205 description: &str,
206 segment: Option<&str>,
207 ) {
208 self.violations.push(Violation {
209 severity,
210 rule: rule.to_string(),
211 description: description.to_string(),
212 segment: segment.map(str::to_string),
213 });
214 }
215
216 fn finish(self, segments_checked: usize) -> AuditReport {
217 AuditReport {
218 violations: self.violations,
219 segments_checked,
220 timestamp: Utc::now(),
221 }
222 }
223}
224
225fn check_hnhbk_rules(segments: &[RawSegment], ctx: &mut AuditContext) {
227 let hnhbk_count = segments
229 .iter()
230 .filter(|s| s.segment_type() == "HNHBK")
231 .count();
232 if hnhbk_count == 0 {
233 ctx.add(
234 ViolationSeverity::Error,
235 "HNHBK-001",
236 "Message has no HNHBK segment (must be first)",
237 Some("HNHBK"),
238 );
239 return; }
241 if hnhbk_count > 1 {
242 ctx.add(
243 ViolationSeverity::Error,
244 "HNHBK-001",
245 "Message has more than one HNHBK segment",
246 Some("HNHBK"),
247 );
248 }
249 if segments[0].segment_type() != "HNHBK" {
250 ctx.add(
251 ViolationSeverity::Error,
252 "HNHBK-001",
253 "HNHBK is not the first segment",
254 Some("HNHBK"),
255 );
256 }
257
258 let hnhbk = &segments[0];
259
260 let size_str = hnhbk.deg(1).get_str(0);
262 match size_str.parse::<u64>() {
263 Ok(size) if size < 50 => {
264 ctx.add(
265 ViolationSeverity::Error,
266 "HNHBK-002",
267 &format!("HNHBK size field is {} (must be >= 50)", size),
268 Some("HNHBK"),
269 );
270 }
271 Err(_) => {
272 ctx.add(
273 ViolationSeverity::Error,
274 "HNHBK-002",
275 &format!("HNHBK size field is not a valid number: {:?}", size_str),
276 Some("HNHBK"),
277 );
278 }
279 _ => {}
280 }
281
282 let version = hnhbk.deg(3).get_str(0);
284 if version != "300" {
285 ctx.add(
286 ViolationSeverity::Error,
287 "HNHBK-003",
288 &format!("FinTS version is {:?} (expected \"300\")", version),
289 Some("HNHBK"),
290 );
291 }
292
293 let msg_num_str = hnhbk.deg(4).get_str(0);
295 match msg_num_str.parse::<u64>() {
296 Ok(n) if n < 1 => {
297 ctx.add(
298 ViolationSeverity::Error,
299 "HNHBK-004",
300 &format!("HNHBK message number is {} (must be >= 1)", n),
301 Some("HNHBK"),
302 );
303 }
304 Err(_) => {
305 ctx.add(
306 ViolationSeverity::Warning,
307 "HNHBK-004",
308 &format!(
309 "HNHBK message number is not a valid number: {:?}",
310 msg_num_str
311 ),
312 Some("HNHBK"),
313 );
314 }
315 _ => {}
316 }
317}
318
319fn check_hnvsk_rules(segments: &[RawSegment], ctx: &mut AuditContext) {
321 let hnvsk = segments.iter().find(|s| s.segment_type() == "HNVSK");
322 match hnvsk {
323 None => {
324 ctx.add(
325 ViolationSeverity::Error,
326 "HNVSK-001",
327 "No HNVSK segment found",
328 Some("HNVSK"),
329 );
330 }
331 Some(seg) if seg.segment_number() != 998 => {
332 ctx.add(
333 ViolationSeverity::Error,
334 "HNVSK-001",
335 &format!(
336 "HNVSK segment number is {} (must be 998)",
337 seg.segment_number()
338 ),
339 Some("HNVSK"),
340 );
341 }
342 _ => {}
343 }
344}
345
346fn check_hnvsd_rules(segments: &[RawSegment], ctx: &mut AuditContext, is_client: bool) {
348 let hnvsd = segments.iter().find(|s| s.segment_type() == "HNVSD");
349 match hnvsd {
350 None => {
351 ctx.add(
352 ViolationSeverity::Error,
353 "HNVSD-001",
354 "No HNVSD segment found",
355 Some("HNVSD"),
356 );
357 return;
358 }
359 Some(seg) if seg.segment_number() != 999 => {
360 ctx.add(
361 ViolationSeverity::Error,
362 "HNVSD-001",
363 &format!(
364 "HNVSD segment number is {} (must be 999)",
365 seg.segment_number()
366 ),
367 Some("HNVSD"),
368 );
369 }
370 _ => {}
371 }
372
373 let hnvsd = hnvsd.unwrap();
375 let payload_opt = hnvsd.deg(1).get(0).as_bytes().map(|b| b.to_vec());
376 match payload_opt {
377 None => {
378 ctx.add(
379 ViolationSeverity::Warning,
380 "HNVSD-002",
381 "HNVSD payload data element is not binary",
382 Some("HNVSD"),
383 );
384 }
385 Some(payload) => match parse_inner_segments(&payload) {
386 Err(e) => {
387 ctx.add(
388 ViolationSeverity::Error,
389 "HNVSD-002",
390 &format!("HNVSD payload is not parseable: {}", e),
391 Some("HNVSD"),
392 );
393 }
394 Ok(inner) => {
395 if is_client {
396 check_inner_client_rules(&inner, ctx);
397 }
398 }
399 },
400 }
401}
402
403fn check_inner_client_rules(inner: &[RawSegment], ctx: &mut AuditContext) {
405 let has_hnshk = inner.iter().any(|s| s.segment_type() == "HNSHK");
406 if !has_hnshk {
407 ctx.add(
408 ViolationSeverity::Error,
409 "HNSHK-001",
410 "Inner segments (HNVSD payload) do not contain HNSHK",
411 Some("HNSHK"),
412 );
413 }
414
415 let has_hnsha = inner.iter().any(|s| s.segment_type() == "HNSHA");
416 if !has_hnsha {
417 ctx.add(
418 ViolationSeverity::Error,
419 "HNSHA-001",
420 "Inner segments (HNVSD payload) do not contain HNSHA",
421 Some("HNSHA"),
422 );
423 }
424
425 let has_hkidn = inner.iter().any(|s| s.segment_type() == "HKIDN");
426 if !has_hkidn {
427 ctx.add(
428 ViolationSeverity::Error,
429 "HKIDN-001",
430 "Inner segments (HNVSD payload) do not contain HKIDN",
431 Some("HKIDN"),
432 );
433 }
434
435 let has_hkvvb = inner.iter().any(|s| s.segment_type() == "HKVVB");
436 if !has_hkvvb {
437 ctx.add(
438 ViolationSeverity::Error,
439 "HKVVB-001",
440 "Inner segments (HNVSD payload) do not contain HKVVB",
441 Some("HKVVB"),
442 );
443 }
444}
445
446fn check_hnhbs_rule(segments: &[RawSegment], ctx: &mut AuditContext) {
448 match segments.last() {
449 None => {
450 ctx.add(
451 ViolationSeverity::Error,
452 "HNHBS-001",
453 "Message has no segments",
454 None,
455 );
456 }
457 Some(last) if last.segment_type() != "HNHBS" => {
458 ctx.add(
459 ViolationSeverity::Error,
460 "HNHBS-001",
461 &format!("Last segment is {:?} (must be HNHBS)", last.segment_type()),
462 Some("HNHBS"),
463 );
464 }
465 _ => {}
466 }
467}
468
469fn check_hirmg_rules(segments: &[RawSegment], ctx: &mut AuditContext) {
471 let hirmg_count = segments
472 .iter()
473 .filter(|s| s.segment_type() == "HIRMG")
474 .count();
475
476 if hirmg_count == 0 {
477 ctx.add(
478 ViolationSeverity::Error,
479 "HIRMG-001",
480 "Server response has no HIRMG segment",
481 Some("HIRMG"),
482 );
483 return;
484 }
485
486 for seg in segments.iter().filter(|s| s.segment_type() == "HIRMG") {
488 let data_degs: Vec<_> = seg.degs.iter().skip(1).collect();
489 if data_degs.is_empty() {
490 ctx.add(
491 ViolationSeverity::Error,
492 "HIRMG-002",
493 "HIRMG has no response code DEGs",
494 Some("HIRMG"),
495 );
496 }
497 }
498}
499
500fn check_response_code_rules(segments: &[RawSegment], ctx: &mut AuditContext) {
502 let code_segs: Vec<_> = segments
503 .iter()
504 .filter(|s| s.segment_type() == "HIRMG" || s.segment_type() == "HIRMS")
505 .collect();
506
507 for seg in &code_segs {
508 let is_global = seg.segment_type() == "HIRMG";
509 let mut success_seen = false;
510 let mut error_seen = false;
511
512 for deg in seg.degs.iter().skip(1) {
513 let code = deg.get_str(0);
514 if code.is_empty() {
515 continue;
516 }
517
518 if code.len() != 4 || !code.chars().all(|c| c.is_ascii_digit()) {
520 ctx.add(
521 ViolationSeverity::Error,
522 "RESP-001",
523 &format!("Response code {:?} is not 4 digits", code),
524 Some(seg.segment_type()),
525 );
526 }
527
528 if let Ok(n) = code.parse::<u16>() {
530 if n < 3000 {
531 success_seen = true;
532 } else if n >= 9000 {
533 error_seen = true;
534 }
535 }
536 }
537
538 if is_global && success_seen && error_seen {
540 ctx.add(
541 ViolationSeverity::Warning,
542 "RESP-002",
543 "HIRMG contains both success (< 3000) and error (>= 9000) codes",
544 Some("HIRMG"),
545 );
546 }
547 }
548}
549
550#[cfg(test)]
553mod tests {
554 use super::*;
555
556 fn minimal_outer(inner_payload: &[u8]) -> Vec<u8> {
558 let payload_len = inner_payload.len();
563 let hnvsd_part = format!("HNVSD:999:1+@{}@", payload_len);
564 let mut msg = Vec::new();
565 msg.extend_from_slice(b"HNHBK:1:3+000000000100+300+0+1+1'");
566 msg.extend_from_slice(b"HNVSK:998:3+998+1+1::0+1:20200101:120000+2:2:13:@8@00000000:5:1+280:12345678:user:V:0:0+0'");
567 msg.extend_from_slice(hnvsd_part.as_bytes());
568 msg.extend_from_slice(inner_payload);
569 msg.push(b'\'');
570 msg.extend_from_slice(b"HNHBS:6:1+1'");
571 msg
572 }
573
574 #[test]
575 fn test_audit_client_no_hnhbk() {
576 let data = b"HNHBS:5:1+1'";
577 let report = audit_client_message(data);
578 let rules: Vec<_> = report.violations.iter().map(|v| v.rule.as_str()).collect();
579 assert!(rules.contains(&"HNHBK-001"), "Expected HNHBK-001 violation");
580 }
581
582 #[test]
583 fn test_audit_report_has_errors_and_warnings() {
584 let data = b"HNHBS:5:1+1'";
585 let report = audit_client_message(data);
586 assert!(report.has_errors());
587 assert!(report.error_count() > 0);
588 }
589
590 #[test]
591 fn test_audit_report_format_contains_rule() {
592 let data = b"HNHBS:5:1+1'";
593 let report = audit_client_message(data);
594 let formatted = report.format_report();
595 assert!(formatted.contains("HNHBK-001"));
596 }
597
598 #[test]
599 fn test_audit_report_to_json() {
600 let data = b"HNHBS:5:1+1'";
601 let report = audit_client_message(data);
602 let json = report.to_json();
603 assert!(json["violations"].is_array());
604 assert!(json["error_count"].as_u64().unwrap() > 0);
605 }
606
607 #[test]
608 fn test_audit_server_response_missing_hirmg() {
609 let data =
611 b"HNHBK:1:3+000000000100+300+0+1+1'HNVSK:998:3+1'HNVSD:999:1+@6@TEST:1'HNHBS:6:1+1'";
612 let report = audit_server_response(data);
613 let rules: Vec<_> = report.violations.iter().map(|v| v.rule.as_str()).collect();
614 assert!(rules.contains(&"HIRMG-001"), "Expected HIRMG-001 violation");
615 }
616
617 #[test]
618 fn test_audit_hnhbs_must_be_last() {
619 let data = b"HNHBK:1:3+000000000100+300+0+1+1'HNHBS:3:1+1'HIRMG:4:2+0010::OK.'";
621 let report = audit_server_response(data);
622 let rules: Vec<_> = report.violations.iter().map(|v| v.rule.as_str()).collect();
623 assert!(rules.contains(&"HNHBS-001"), "Expected HNHBS-001 violation");
624 }
625
626 #[test]
627 fn test_audit_resp001_bad_code() {
628 let data = b"HNHBK:1:3+000000000100+300+0+1+1'HIRMG:3:2+XYZ::Bad code.'HNHBS:4:1+1'";
629 let report = audit_server_response(data);
630 let rules: Vec<_> = report.violations.iter().map(|v| v.rule.as_str()).collect();
631 assert!(rules.contains(&"RESP-001"), "Expected RESP-001 violation");
632 }
633
634 #[test]
635 fn test_hnhbk_version_rule() {
636 let data = b"HNHBK:1:3+000000000100+200+0+1+1'HNHBS:2:1+1'";
638 let report = audit_client_message(data);
639 let rules: Vec<_> = report.violations.iter().map(|v| v.rule.as_str()).collect();
640 assert!(rules.contains(&"HNHBK-003"), "Expected HNHBK-003 violation");
641 }
642
643 #[test]
644 fn test_violation_severity_as_str() {
645 assert_eq!(ViolationSeverity::Error.as_str(), "ERROR");
646 assert_eq!(ViolationSeverity::Warning.as_str(), "WARNING");
647 assert_eq!(ViolationSeverity::Info.as_str(), "INFO");
648 }
649}