Skip to main content

async_snmp/v3/
report.rs

1//! Typed classification of SNMPv3 Report PDUs.
2
3use crate::pdu::{Pdu, PduType};
4use crate::{Oid, Value};
5
6use super::report_oids;
7
8/// Status carried by a structurally valid SNMPv3 Report PDU.
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum ReportStatus {
12    /// The requested security level is unsupported for the selected user.
13    UnsupportedSecurityLevel { counter: u32 },
14    /// The authenticated message was outside the authoritative engine's time window.
15    NotInTimeWindow { counter: u32 },
16    /// The requested USM security name is unknown.
17    UnknownUserName { counter: u32 },
18    /// The authoritative engine ID is unknown.
19    UnknownEngineId { counter: u32 },
20    /// Authentication verification failed.
21    WrongDigest { counter: u32 },
22    /// Scoped-PDU decryption failed.
23    DecryptionError { counter: u32 },
24    /// A non-USM or otherwise unrecognized Report status.
25    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/// Error returned when a PDU does not have the exact shape required for a Report.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
54#[error("malformed SNMPv3 Report PDU")]
55pub struct MalformedReport;
56
57/// Classify an SNMPv3 Report PDU after security processing and message correlation.
58///
59/// A valid Report has zero error status/index and exactly one varbind. Standard
60/// USM report objects must carry a `Counter32`; unknown report objects preserve
61/// their value in [`ReportStatus::Other`].
62pub 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}