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 OwaspParseError {
11 Empty,
12 Unknown,
13}
14
15impl fmt::Display for OwaspParseError {
16 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 Self::Empty => formatter.write_str("OWASP label cannot be empty"),
19 Self::Unknown => formatter.write_str("unknown OWASP label"),
20 }
21 }
22}
23
24impl Error for OwaspParseError {}
25
26macro_rules! label_enum {
27 ($name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
28 impl $name {
29 #[must_use]
31 pub const fn as_str(self) -> &'static str {
32 match self {
33 $(Self::$variant => $label,)+
34 }
35 }
36 }
37
38 impl fmt::Display for $name {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 formatter.write_str(self.as_str())
41 }
42 }
43
44 impl FromStr for $name {
45 type Err = OwaspParseError;
46
47 fn from_str(input: &str) -> Result<Self, Self::Err> {
48 let trimmed = input.trim();
49 if trimmed.is_empty() {
50 return Err(OwaspParseError::Empty);
51 }
52 let normalized = trimmed.to_ascii_lowercase();
53 match normalized.as_str() {
54 $($label => Ok(Self::$variant),)+
55 _ => Err(OwaspParseError::Unknown),
56 }
57 }
58 }
59 };
60}
61
62#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
64pub enum OwaspTop10Version {
65 Top10_2017,
66 Top10_2021,
67 Top10_2025,
68}
69
70label_enum!(OwaspTop10Version {
71 Top10_2017 => "top-10-2017",
72 Top10_2021 => "top-10-2021",
73 Top10_2025 => "top-10-2025",
74});
75
76#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
78pub enum OwaspTop10Category {
79 BrokenAccessControl,
80 CryptographicFailures,
81 Injection,
82 InsecureDesign,
83 SecurityMisconfiguration,
84 VulnerableAndOutdatedComponents,
85 IdentificationAndAuthenticationFailures,
86 SoftwareAndDataIntegrityFailures,
87 SecurityLoggingAndMonitoringFailures,
88 ServerSideRequestForgery,
89 Other,
90}
91
92label_enum!(OwaspTop10Category {
93 BrokenAccessControl => "broken-access-control",
94 CryptographicFailures => "cryptographic-failures",
95 Injection => "injection",
96 InsecureDesign => "insecure-design",
97 SecurityMisconfiguration => "security-misconfiguration",
98 VulnerableAndOutdatedComponents => "vulnerable-and-outdated-components",
99 IdentificationAndAuthenticationFailures => "identification-and-authentication-failures",
100 SoftwareAndDataIntegrityFailures => "software-and-data-integrity-failures",
101 SecurityLoggingAndMonitoringFailures => "security-logging-and-monitoring-failures",
102 ServerSideRequestForgery => "server-side-request-forgery",
103 Other => "other",
104});
105
106#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub struct OwaspRiskId(String);
109
110impl OwaspRiskId {
111 pub fn new(input: impl AsRef<str>) -> Result<Self, OwaspTextError> {
113 let trimmed = input.as_ref().trim();
114 if trimmed.is_empty() {
115 Err(OwaspTextError::Empty)
116 } else {
117 Ok(Self(trimmed.to_owned()))
118 }
119 }
120
121 #[must_use]
123 pub fn as_str(&self) -> &str {
124 &self.0
125 }
126}
127
128impl fmt::Display for OwaspRiskId {
129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 formatter.write_str(self.as_str())
131 }
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub enum OwaspTextError {
137 Empty,
138}
139
140impl fmt::Display for OwaspTextError {
141 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
142 formatter.write_str("OWASP metadata text cannot be empty")
143 }
144}
145
146impl Error for OwaspTextError {}
147
148#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
150pub enum OwaspProjectKind {
151 Top10,
152 Asvs,
153 Masvs,
154 CheatSheet,
155 DependencyTrack,
156 Zap,
157}
158
159label_enum!(OwaspProjectKind {
160 Top10 => "top-10",
161 Asvs => "asvs",
162 Masvs => "masvs",
163 CheatSheet => "cheat-sheet",
164 DependencyTrack => "dependency-track",
165 Zap => "zap",
166});
167
168#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
170pub enum OwaspControlArea {
171 AccessControl,
172 Authentication,
173 Cryptography,
174 InputValidation,
175 LoggingAndMonitoring,
176 SecureConfiguration,
177 SupplyChain,
178 Other,
179}
180
181label_enum!(OwaspControlArea {
182 AccessControl => "access-control",
183 Authentication => "authentication",
184 Cryptography => "cryptography",
185 InputValidation => "input-validation",
186 LoggingAndMonitoring => "logging-and-monitoring",
187 SecureConfiguration => "secure-configuration",
188 SupplyChain => "supply-chain",
189 Other => "other",
190});
191
192#[cfg(test)]
193mod tests {
194 use super::{OwaspProjectKind, OwaspRiskId, OwaspTop10Category, OwaspTop10Version};
195
196 #[test]
197 fn parses_and_displays_top_10_category() {
198 assert_eq!(
199 "broken-access-control"
200 .parse::<OwaspTop10Category>()
201 .expect("category"),
202 OwaspTop10Category::BrokenAccessControl
203 );
204 assert_eq!(
205 OwaspTop10Category::ServerSideRequestForgery.to_string(),
206 "server-side-request-forgery"
207 );
208 }
209
210 #[test]
211 fn exposes_version_and_project_labels() {
212 assert_eq!(OwaspTop10Version::Top10_2021.to_string(), "top-10-2021");
213 assert_eq!(OwaspProjectKind::CheatSheet.to_string(), "cheat-sheet");
214 }
215
216 #[test]
217 fn validates_risk_id() {
218 let id = OwaspRiskId::new("A01:2021").expect("risk id");
219
220 assert_eq!(id.as_str(), "A01:2021");
221 assert!(OwaspRiskId::new(" ").is_err());
222 }
223}