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#[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#[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub struct CweNumber(u32);
53
54impl CweNumber {
55 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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
87pub struct CweId {
88 number: CweNumber,
89}
90
91impl CweId {
92 #[must_use]
94 pub const fn from_number(number: CweNumber) -> Self {
95 Self { number }
96 }
97
98 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 #[must_use]
115 pub const fn number(self) -> CweNumber {
116 self.number
117 }
118
119 #[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 #[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#[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#[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#[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#[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}