Skip to main content

use_threat/
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 threat metadata is invalid.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum ThreatError {
11    Empty,
12    Unknown,
13}
14
15impl fmt::Display for ThreatError {
16    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
17        match self {
18            Self::Empty => formatter.write_str("threat metadata cannot be empty"),
19            Self::Unknown => formatter.write_str("unknown threat label"),
20        }
21    }
22}
23
24impl Error for ThreatError {}
25
26macro_rules! text_newtype {
27    ($name:ident) => {
28        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29        pub struct $name(String);
30
31        impl $name {
32            /// Creates non-empty threat text metadata.
33            pub fn new(input: impl AsRef<str>) -> Result<Self, ThreatError> {
34                let trimmed = input.as_ref().trim();
35                if trimmed.is_empty() {
36                    Err(ThreatError::Empty)
37                } else {
38                    Ok(Self(trimmed.to_owned()))
39                }
40            }
41
42            /// Returns the stored text.
43            #[must_use]
44            pub fn as_str(&self) -> &str {
45                &self.0
46            }
47        }
48
49        impl fmt::Display for $name {
50            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51                formatter.write_str(self.as_str())
52            }
53        }
54
55        impl FromStr for $name {
56            type Err = ThreatError;
57
58            fn from_str(input: &str) -> Result<Self, Self::Err> {
59                Self::new(input)
60            }
61        }
62
63        impl TryFrom<&str> for $name {
64            type Error = ThreatError;
65
66            fn try_from(value: &str) -> Result<Self, Self::Error> {
67                Self::new(value)
68            }
69        }
70    };
71}
72
73macro_rules! label_enum {
74    ($name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
75        impl $name {
76            /// Returns the stable label.
77            #[must_use]
78            pub const fn as_str(self) -> &'static str {
79                match self {
80                    $(Self::$variant => $label,)+
81                }
82            }
83        }
84
85        impl fmt::Display for $name {
86            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87                formatter.write_str(self.as_str())
88            }
89        }
90
91        impl FromStr for $name {
92            type Err = ThreatError;
93
94            fn from_str(input: &str) -> Result<Self, Self::Err> {
95                let trimmed = input.trim();
96                if trimmed.is_empty() {
97                    return Err(ThreatError::Empty);
98                }
99                let normalized = trimmed.to_ascii_lowercase();
100                match normalized.as_str() {
101                    $($label => Ok(Self::$variant),)+
102                    _ => Err(ThreatError::Unknown),
103                }
104            }
105        }
106    };
107}
108
109text_newtype!(ThreatId);
110text_newtype!(ThreatSurface);
111
112/// Threat actor labels.
113#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
114pub enum ThreatActorKind {
115    External,
116    Insider,
117    ThirdParty,
118    Automated,
119    NationState,
120    Criminal,
121    Researcher,
122    Unknown,
123}
124
125label_enum!(ThreatActorKind {
126    External => "external",
127    Insider => "insider",
128    ThirdParty => "third-party",
129    Automated => "automated",
130    NationState => "nation-state",
131    Criminal => "criminal",
132    Researcher => "researcher",
133    Unknown => "unknown",
134});
135
136/// Threat category labels.
137#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
138pub enum ThreatCategory {
139    Spoofing,
140    Tampering,
141    Repudiation,
142    InformationDisclosure,
143    DenialOfService,
144    ElevationOfPrivilege,
145    SupplyChain,
146    SocialEngineering,
147    Other,
148}
149
150label_enum!(ThreatCategory {
151    Spoofing => "spoofing",
152    Tampering => "tampering",
153    Repudiation => "repudiation",
154    InformationDisclosure => "information-disclosure",
155    DenialOfService => "denial-of-service",
156    ElevationOfPrivilege => "elevation-of-privilege",
157    SupplyChain => "supply-chain",
158    SocialEngineering => "social-engineering",
159    Other => "other",
160});
161
162/// Threat capability labels.
163#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
164pub enum ThreatCapability {
165    Low,
166    Medium,
167    High,
168    Advanced,
169}
170
171label_enum!(ThreatCapability {
172    Low => "low",
173    Medium => "medium",
174    High => "high",
175    Advanced => "advanced",
176});
177
178/// Threat intent labels.
179#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
180pub enum ThreatIntent {
181    Curious,
182    Opportunistic,
183    Targeted,
184    Malicious,
185    Unknown,
186}
187
188label_enum!(ThreatIntent {
189    Curious => "curious",
190    Opportunistic => "opportunistic",
191    Targeted => "targeted",
192    Malicious => "malicious",
193    Unknown => "unknown",
194});
195
196/// Threat model kind labels.
197#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
198pub enum ThreatModelKind {
199    Stride,
200    AttackTree,
201    KillChain,
202    MitreAttackLike,
203    Custom,
204}
205
206label_enum!(ThreatModelKind {
207    Stride => "stride",
208    AttackTree => "attack-tree",
209    KillChain => "kill-chain",
210    MitreAttackLike => "mitre-attack-like",
211    Custom => "custom",
212});
213
214/// A compact threat scenario metadata record.
215#[derive(Clone, Debug, Eq, PartialEq)]
216pub struct ThreatScenario {
217    id: ThreatId,
218    category: ThreatCategory,
219    actor: ThreatActorKind,
220}
221
222impl ThreatScenario {
223    /// Creates threat scenario metadata.
224    #[must_use]
225    pub const fn new(id: ThreatId, category: ThreatCategory, actor: ThreatActorKind) -> Self {
226        Self {
227            id,
228            category,
229            actor,
230        }
231    }
232
233    /// Returns the scenario ID.
234    #[must_use]
235    pub const fn id(&self) -> &ThreatId {
236        &self.id
237    }
238
239    /// Returns the threat category.
240    #[must_use]
241    pub const fn category(&self) -> ThreatCategory {
242        self.category
243    }
244
245    /// Returns the actor kind.
246    #[must_use]
247    pub const fn actor(&self) -> ThreatActorKind {
248        self.actor
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::{ThreatActorKind, ThreatCategory, ThreatId, ThreatModelKind, ThreatScenario};
255
256    #[test]
257    fn validates_threat_id() {
258        let id = ThreatId::new("T-1").expect("threat id");
259
260        assert_eq!(id.as_str(), "T-1");
261        assert!(ThreatId::new(" ").is_err());
262    }
263
264    #[test]
265    fn parses_and_displays_labels() {
266        assert_eq!(
267            "spoofing".parse::<ThreatCategory>().expect("category"),
268            ThreatCategory::Spoofing
269        );
270        assert_eq!(ThreatActorKind::ThirdParty.to_string(), "third-party");
271        assert_eq!(ThreatModelKind::Stride.to_string(), "stride");
272    }
273
274    #[test]
275    fn scenario_reports_metadata() {
276        let scenario = ThreatScenario::new(
277            ThreatId::new("T-1").expect("threat id"),
278            ThreatCategory::Spoofing,
279            ThreatActorKind::External,
280        );
281
282        assert_eq!(scenario.id().as_str(), "T-1");
283        assert_eq!(scenario.category(), ThreatCategory::Spoofing);
284    }
285}