Skip to main content

use_owasp/
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 an OWASP label cannot be parsed.
9#[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            /// Returns the stable label.
30            #[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/// OWASP Top 10 version labels.
63#[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/// OWASP Top 10 style category labels.
77#[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/// Lightweight OWASP risk identifier.
107#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub struct OwaspRiskId(String);
109
110impl OwaspRiskId {
111    /// Creates a non-empty OWASP risk ID.
112    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    /// Returns the stored risk ID.
122    #[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/// Error returned when OWASP text metadata is invalid.
135#[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/// OWASP project labels.
149#[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/// Application security control-area labels.
169#[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}