1use crate::pdu::{Pdu, PduType};
4use crate::{Oid, Value};
5
6use super::report_oids;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum ReportStatus {
12 UnsupportedSecurityLevel { counter: u32 },
14 NotInTimeWindow { counter: u32 },
16 UnknownUserName { counter: u32 },
18 UnknownEngineId { counter: u32 },
20 WrongDigest { counter: u32 },
22 DecryptionError { counter: u32 },
24 Other { oid: Oid, value: Value },
26}
27
28impl std::fmt::Display for ReportStatus {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Self::UnsupportedSecurityLevel { counter } => {
32 write!(f, "unsupported security level (counter {counter})")
33 }
34 Self::NotInTimeWindow { counter } => {
35 write!(f, "message outside time window (counter {counter})")
36 }
37 Self::UnknownUserName { counter } => {
38 write!(f, "unknown user name (counter {counter})")
39 }
40 Self::UnknownEngineId { counter } => {
41 write!(f, "unknown engine ID (counter {counter})")
42 }
43 Self::WrongDigest { counter } => write!(f, "wrong digest (counter {counter})"),
44 Self::DecryptionError { counter } => {
45 write!(f, "decryption error (counter {counter})")
46 }
47 Self::Other { oid, .. } => write!(f, "unrecognized Report status {oid}"),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
54#[error("malformed SNMPv3 Report PDU")]
55pub struct MalformedReport;
56
57pub fn classify_report(pdu: &Pdu) -> Result<ReportStatus, MalformedReport> {
63 if pdu.pdu_type != PduType::Report || pdu.error_status != 0 || pdu.error_index != 0 {
64 return Err(MalformedReport);
65 }
66
67 let [varbind] = pdu.varbinds.as_slice() else {
68 return Err(MalformedReport);
69 };
70
71 let standard_status = if varbind.oid == report_oids::unsupported_sec_levels() {
72 Some(ReportStatus::UnsupportedSecurityLevel {
73 counter: counter32(&varbind.value)?,
74 })
75 } else if varbind.oid == report_oids::not_in_time_windows() {
76 Some(ReportStatus::NotInTimeWindow {
77 counter: counter32(&varbind.value)?,
78 })
79 } else if varbind.oid == report_oids::unknown_user_names() {
80 Some(ReportStatus::UnknownUserName {
81 counter: counter32(&varbind.value)?,
82 })
83 } else if varbind.oid == report_oids::unknown_engine_ids() {
84 Some(ReportStatus::UnknownEngineId {
85 counter: counter32(&varbind.value)?,
86 })
87 } else if varbind.oid == report_oids::wrong_digests() {
88 Some(ReportStatus::WrongDigest {
89 counter: counter32(&varbind.value)?,
90 })
91 } else if varbind.oid == report_oids::decryption_errors() {
92 Some(ReportStatus::DecryptionError {
93 counter: counter32(&varbind.value)?,
94 })
95 } else {
96 None
97 };
98
99 Ok(standard_status.unwrap_or_else(|| ReportStatus::Other {
100 oid: varbind.oid.clone(),
101 value: varbind.value.clone(),
102 }))
103}
104
105fn counter32(value: &Value) -> Result<u32, MalformedReport> {
106 match value {
107 Value::Counter32(counter) => Ok(*counter),
108 _ => Err(MalformedReport),
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::{VarBind, oid};
116
117 fn report(oid: Oid, value: Value) -> Pdu {
118 Pdu {
119 pdu_type: PduType::Report,
120 request_id: 1,
121 error_status: 0,
122 error_index: 0,
123 varbinds: vec![VarBind::new(oid, value)],
124 }
125 }
126
127 #[test]
128 fn classifies_every_standard_usm_status() {
129 let cases = [
130 (
131 report_oids::unsupported_sec_levels(),
132 ReportStatus::UnsupportedSecurityLevel { counter: 7 },
133 ),
134 (
135 report_oids::not_in_time_windows(),
136 ReportStatus::NotInTimeWindow { counter: 7 },
137 ),
138 (
139 report_oids::unknown_user_names(),
140 ReportStatus::UnknownUserName { counter: 7 },
141 ),
142 (
143 report_oids::unknown_engine_ids(),
144 ReportStatus::UnknownEngineId { counter: 7 },
145 ),
146 (
147 report_oids::wrong_digests(),
148 ReportStatus::WrongDigest { counter: 7 },
149 ),
150 (
151 report_oids::decryption_errors(),
152 ReportStatus::DecryptionError { counter: 7 },
153 ),
154 ];
155
156 for (oid, expected) in cases {
157 assert_eq!(
158 classify_report(&report(oid, Value::Counter32(7))),
159 Ok(expected)
160 );
161 }
162 }
163
164 #[test]
165 fn preserves_unknown_report_status() {
166 let oid = oid!(1, 3, 6, 1, 6, 3, 12, 1, 5, 0);
167 assert_eq!(
168 classify_report(&report(oid.clone(), Value::Integer(9))),
169 Ok(ReportStatus::Other {
170 oid,
171 value: Value::Integer(9),
172 })
173 );
174 }
175
176 #[test]
177 fn rejects_malformed_report_shapes() {
178 let valid = report(report_oids::not_in_time_windows(), Value::Counter32(1));
179
180 let mut non_report = valid.clone();
181 non_report.pdu_type = PduType::Response;
182 assert_eq!(classify_report(&non_report), Err(MalformedReport));
183
184 let mut status = valid.clone();
185 status.error_status = 1;
186 assert_eq!(classify_report(&status), Err(MalformedReport));
187
188 let mut index = valid.clone();
189 index.error_index = 1;
190 assert_eq!(classify_report(&index), Err(MalformedReport));
191
192 let mut empty = valid.clone();
193 empty.varbinds.clear();
194 assert_eq!(classify_report(&empty), Err(MalformedReport));
195
196 let mut multiple = valid.clone();
197 multiple.varbinds.push(VarBind::new(
198 report_oids::wrong_digests(),
199 Value::Counter32(2),
200 ));
201 assert_eq!(classify_report(&multiple), Err(MalformedReport));
202
203 let wrong_type = report(report_oids::not_in_time_windows(), Value::Integer(1));
204 assert_eq!(classify_report(&wrong_type), Err(MalformedReport));
205 }
206}