Skip to main content

use_cwe/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3#![allow(clippy::module_name_repetitions)]
4
5use core::{fmt, str::FromStr};
6use std::error::Error;
7
8/// Error returned when a CWE identifier is invalid.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum CweIdError {
11    Empty,
12    InvalidPrefix,
13    InvalidFormat,
14    InvalidNumber,
15}
16
17impl fmt::Display for CweIdError {
18    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            Self::Empty => formatter.write_str("CWE identifier cannot be empty"),
21            Self::InvalidPrefix => {
22                formatter.write_str("CWE identifier must start with uppercase CWE")
23            }
24            Self::InvalidFormat => formatter.write_str("CWE identifier must match CWE-N"),
25            Self::InvalidNumber => formatter.write_str("CWE number must be ASCII digits"),
26        }
27    }
28}
29
30impl Error for CweIdError {}
31
32/// Error returned when a CWE label cannot be parsed.
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum CweParseError {
35    Empty,
36    Unknown,
37}
38
39impl fmt::Display for CweParseError {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::Empty => formatter.write_str("CWE label cannot be empty"),
43            Self::Unknown => formatter.write_str("unknown CWE label"),
44        }
45    }
46}
47
48impl Error for CweParseError {}
49
50/// Numeric CWE identifier component.
51#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub struct CweNumber(u32);
53
54impl CweNumber {
55    /// Creates a non-zero CWE number.
56    pub const fn new(value: u32) -> Result<Self, CweIdError> {
57        if value == 0 {
58            Err(CweIdError::InvalidNumber)
59        } else {
60            Ok(Self(value))
61        }
62    }
63
64    /// Returns the numeric CWE value.
65    #[must_use]
66    pub const fn value(self) -> u32 {
67        self.0
68    }
69}
70
71impl fmt::Display for CweNumber {
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        write!(formatter, "{}", self.0)
74    }
75}
76
77impl FromStr for CweNumber {
78    type Err = CweIdError;
79
80    fn from_str(input: &str) -> Result<Self, Self::Err> {
81        parse_number(input)
82    }
83}
84
85/// A validated CWE identifier such as `CWE-79`.
86#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
87pub struct CweId {
88    number: CweNumber,
89}
90
91impl CweId {
92    /// Creates a CWE identifier from a numeric component.
93    #[must_use]
94    pub const fn from_number(number: CweNumber) -> Self {
95        Self { number }
96    }
97
98    /// Creates a validated CWE identifier.
99    pub fn new(input: impl AsRef<str>) -> Result<Self, CweIdError> {
100        let trimmed = input.as_ref().trim();
101        if trimmed.is_empty() {
102            return Err(CweIdError::Empty);
103        }
104        let (prefix, number) = trimmed.split_once('-').ok_or(CweIdError::InvalidFormat)?;
105        if prefix != "CWE" {
106            return Err(CweIdError::InvalidPrefix);
107        }
108        Ok(Self {
109            number: parse_number(number)?,
110        })
111    }
112
113    /// Returns the numeric CWE component.
114    #[must_use]
115    pub const fn number(self) -> CweNumber {
116        self.number
117    }
118
119    /// Returns an owned CWE identifier string.
120    #[must_use]
121    pub fn as_str(&self) -> String {
122        format!("CWE-{}", self.number.value())
123    }
124}
125
126impl fmt::Display for CweId {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        write!(formatter, "CWE-{}", self.number.value())
129    }
130}
131
132impl FromStr for CweId {
133    type Err = CweIdError;
134
135    fn from_str(input: &str) -> Result<Self, Self::Err> {
136        Self::new(input)
137    }
138}
139
140impl TryFrom<&str> for CweId {
141    type Error = CweIdError;
142
143    fn try_from(value: &str) -> Result<Self, Self::Error> {
144        Self::new(value)
145    }
146}
147
148pub const CWE_79_XSS: CweId = CweId::from_number(CweNumber(79));
149pub const CWE_89_SQL_INJECTION: CweId = CweId::from_number(CweNumber(89));
150pub const CWE_352_CSRF: CweId = CweId::from_number(CweNumber(352));
151pub const CWE_862_MISSING_AUTHORIZATION: CweId = CweId::from_number(CweNumber(862));
152pub const CWE_287_IMPROPER_AUTHENTICATION: CweId = CweId::from_number(CweNumber(287));
153pub const CWE_22_PATH_TRAVERSAL: CweId = CweId::from_number(CweNumber(22));
154pub const CWE_78_OS_COMMAND_INJECTION: CweId = CweId::from_number(CweNumber(78));
155pub const CWE_94_CODE_INJECTION: CweId = CweId::from_number(CweNumber(94));
156pub const CWE_200_SENSITIVE_INFORMATION_EXPOSURE: CweId = CweId::from_number(CweNumber(200));
157pub const CWE_918_SSRF: CweId = CweId::from_number(CweNumber(918));
158
159macro_rules! label_enum {
160    ($name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
161        impl $name {
162            /// Returns the stable label.
163            #[must_use]
164            pub const fn as_str(self) -> &'static str {
165                match self {
166                    $(Self::$variant => $label,)+
167                }
168            }
169        }
170
171        impl fmt::Display for $name {
172            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173                formatter.write_str(self.as_str())
174            }
175        }
176
177        impl FromStr for $name {
178            type Err = CweParseError;
179
180            fn from_str(input: &str) -> Result<Self, Self::Err> {
181                let trimmed = input.trim();
182                if trimmed.is_empty() {
183                    return Err(CweParseError::Empty);
184                }
185                let normalized = trimmed.to_ascii_lowercase();
186                match normalized.as_str() {
187                    $($label => Ok(Self::$variant),)+
188                    _ => Err(CweParseError::Unknown),
189                }
190            }
191        }
192    };
193}
194
195/// CWE weakness category labels.
196#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
197pub enum CweWeaknessKind {
198    Injection,
199    CrossSiteScripting,
200    CrossSiteRequestForgery,
201    MissingAuthorization,
202    MissingAuthentication,
203    PathTraversal,
204    CommandInjection,
205    CodeInjection,
206    BufferOverflow,
207    OutOfBoundsRead,
208    OutOfBoundsWrite,
209    UseAfterFree,
210    SensitiveInformationExposure,
211    Ssrf,
212    ResourceExhaustion,
213    Other,
214}
215
216label_enum!(CweWeaknessKind {
217    Injection => "injection",
218    CrossSiteScripting => "cross-site-scripting",
219    CrossSiteRequestForgery => "cross-site-request-forgery",
220    MissingAuthorization => "missing-authorization",
221    MissingAuthentication => "missing-authentication",
222    PathTraversal => "path-traversal",
223    CommandInjection => "command-injection",
224    CodeInjection => "code-injection",
225    BufferOverflow => "buffer-overflow",
226    OutOfBoundsRead => "out-of-bounds-read",
227    OutOfBoundsWrite => "out-of-bounds-write",
228    UseAfterFree => "use-after-free",
229    SensitiveInformationExposure => "sensitive-information-exposure",
230    Ssrf => "ssrf",
231    ResourceExhaustion => "resource-exhaustion",
232    Other => "other",
233});
234
235/// CWE impact category labels.
236#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
237pub enum CweImpactKind {
238    Confidentiality,
239    Integrity,
240    Availability,
241    AccessControl,
242    Accountability,
243    Other,
244}
245
246label_enum!(CweImpactKind {
247    Confidentiality => "confidentiality",
248    Integrity => "integrity",
249    Availability => "availability",
250    AccessControl => "access-control",
251    Accountability => "accountability",
252    Other => "other",
253});
254
255/// CWE likelihood labels.
256#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
257pub enum CweLikelihood {
258    Low,
259    Medium,
260    High,
261    Unknown,
262}
263
264label_enum!(CweLikelihood {
265    Low => "low",
266    Medium => "medium",
267    High => "high",
268    Unknown => "unknown",
269});
270
271/// CWE taxonomy source labels.
272#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
273pub enum CweTaxonomySource {
274    Cwe,
275    Owasp,
276    Nist,
277    Custom,
278}
279
280label_enum!(CweTaxonomySource {
281    Cwe => "cwe",
282    Owasp => "owasp",
283    Nist => "nist",
284    Custom => "custom",
285});
286
287fn parse_number(input: &str) -> Result<CweNumber, CweIdError> {
288    if input.is_empty() || !input.bytes().all(|byte| byte.is_ascii_digit()) {
289        return Err(CweIdError::InvalidNumber);
290    }
291    let value = input
292        .parse::<u32>()
293        .map_err(|_error| CweIdError::InvalidNumber)?;
294    CweNumber::new(value)
295}
296
297#[cfg(test)]
298mod tests {
299    use super::{
300        CWE_79_XSS, CWE_89_SQL_INJECTION, CWE_352_CSRF, CweId, CweIdError, CweWeaknessKind,
301    };
302
303    #[test]
304    fn parses_valid_cwe_id() {
305        let id: CweId = "CWE-79".parse().expect("valid CWE should parse");
306
307        assert_eq!(id, CWE_79_XSS);
308        assert_eq!(id.number().value(), 79);
309        assert_eq!(id.to_string(), "CWE-79");
310    }
311
312    #[test]
313    fn rejects_invalid_cwe_ids() {
314        assert_eq!(CweId::new(""), Err(CweIdError::Empty));
315        assert_eq!(CweId::new("cwe-79"), Err(CweIdError::InvalidPrefix));
316        assert_eq!(CweId::new("CWE"), Err(CweIdError::InvalidFormat));
317        assert_eq!(CweId::new("CWE-"), Err(CweIdError::InvalidNumber));
318        assert_eq!(CweId::new("CWE-7A"), Err(CweIdError::InvalidNumber));
319    }
320
321    #[test]
322    fn exposes_common_constants() {
323        assert_eq!(CWE_89_SQL_INJECTION.to_string(), "CWE-89");
324        assert_eq!(CWE_352_CSRF.to_string(), "CWE-352");
325    }
326
327    #[test]
328    fn parses_and_displays_weakness_kind() {
329        assert_eq!(
330            "cross-site-scripting"
331                .parse::<CweWeaknessKind>()
332                .expect("weakness"),
333            CweWeaknessKind::CrossSiteScripting
334        );
335        assert_eq!(CweWeaknessKind::Ssrf.to_string(), "ssrf");
336    }
337}