threatflux_string_analysis/
types.rs1use thiserror::Error;
4
5pub const DEFAULT_SUSPICIOUS_ENTROPY: f64 = 4.5;
7pub const DEFAULT_MAX_OCCURRENCES_PER_STRING: usize = 1_000;
9pub const DEFAULT_MAX_UNIQUE_STRINGS: usize = 100_000;
11pub const DEFAULT_MAX_INPUT_BYTES: usize = 1_048_576;
13pub const DEFAULT_MAX_SOURCE_BYTES: usize = 16_384;
15pub const DEFAULT_MAX_UNIQUE_FILE_IDENTITIES_PER_STRING: usize = 1_024;
17pub const DEFAULT_MAX_CATEGORIES_PER_STRING: usize = 64;
19pub const DEFAULT_MAX_INDICATORS_PER_STRING: usize = 64;
21
22#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
28#[serde(default, deny_unknown_fields)]
29pub struct AnalysisConfig {
30 pub min_suspicious_entropy: f64,
32 pub max_occurrences_per_string: usize,
34 pub max_unique_strings: usize,
36 pub max_input_bytes: usize,
38 pub max_source_bytes: usize,
40 pub max_unique_file_identities_per_string: usize,
42 pub max_categories_per_string: usize,
44 pub max_indicators_per_string: usize,
46}
47
48impl AnalysisConfig {
49 pub fn validate(&self) -> AnalysisResult<()> {
51 if !self.min_suspicious_entropy.is_finite()
52 || !(0.0..=8.0).contains(&self.min_suspicious_entropy)
53 {
54 return Err(AnalysisError::InvalidConfiguration {
55 field: "min_suspicious_entropy",
56 reason: "must be finite and between 0 and 8 inclusive".to_string(),
57 });
58 }
59
60 for (field, value) in [
61 (
62 "max_occurrences_per_string",
63 self.max_occurrences_per_string,
64 ),
65 ("max_unique_strings", self.max_unique_strings),
66 ("max_input_bytes", self.max_input_bytes),
67 ("max_source_bytes", self.max_source_bytes),
68 (
69 "max_unique_file_identities_per_string",
70 self.max_unique_file_identities_per_string,
71 ),
72 ("max_categories_per_string", self.max_categories_per_string),
73 ("max_indicators_per_string", self.max_indicators_per_string),
74 ] {
75 if value == 0 {
76 return Err(AnalysisError::InvalidConfiguration {
77 field,
78 reason: "must be greater than zero".to_string(),
79 });
80 }
81 }
82
83 Ok(())
84 }
85}
86
87impl Default for AnalysisConfig {
88 fn default() -> Self {
89 Self {
90 min_suspicious_entropy: DEFAULT_SUSPICIOUS_ENTROPY,
91 max_occurrences_per_string: DEFAULT_MAX_OCCURRENCES_PER_STRING,
92 max_unique_strings: DEFAULT_MAX_UNIQUE_STRINGS,
93 max_input_bytes: DEFAULT_MAX_INPUT_BYTES,
94 max_source_bytes: DEFAULT_MAX_SOURCE_BYTES,
95 max_unique_file_identities_per_string: DEFAULT_MAX_UNIQUE_FILE_IDENTITIES_PER_STRING,
96 max_categories_per_string: DEFAULT_MAX_CATEGORIES_PER_STRING,
97 max_indicators_per_string: DEFAULT_MAX_INDICATORS_PER_STRING,
98 }
99 }
100}
101
102#[derive(Debug, Error)]
104#[non_exhaustive]
105pub enum AnalysisError {
106 #[error("invalid configuration `{field}`: {reason}")]
108 InvalidConfiguration {
109 field: &'static str,
111 reason: String,
113 },
114
115 #[error("invalid filter `{field}`: {reason}")]
117 InvalidFilter {
118 field: &'static str,
120 reason: String,
122 },
123
124 #[error("invalid component output `{field}`: {reason}")]
126 InvalidComponentOutput {
127 field: &'static str,
129 reason: String,
131 },
132
133 #[error("`{field}` is {actual} bytes; configured maximum is {limit}")]
135 InputTooLarge {
136 field: &'static str,
138 actual: usize,
140 limit: usize,
142 },
143
144 #[error("capacity exceeded for {resource}: configured maximum is {limit}")]
146 CapacityExceeded {
147 resource: &'static str,
149 limit: usize,
151 },
152
153 #[error("invalid {kind} identifier: {reason}")]
155 InvalidIdentifier {
156 kind: &'static str,
158 name: String,
160 reason: &'static str,
162 },
163
164 #[error("invalid severity {severity}; expected a value from 0 through 10")]
166 InvalidSeverity {
167 severity: u8,
169 },
170
171 #[error("duplicate {kind} name")]
173 DuplicateName {
174 kind: &'static str,
176 name: String,
178 },
179
180 #[error("{kind} was not found")]
182 NotFound {
183 kind: &'static str,
185 name: String,
187 },
188
189 #[error("invalid regular expression for {context}: {reason}")]
191 InvalidRegex {
192 context: &'static str,
194 reason: &'static str,
196 },
197}
198
199pub type AnalysisResult<T> = Result<T, AnalysisError>;
201
202pub(crate) const MAX_IDENTIFIER_BYTES: usize = 256;
203pub(crate) const MAX_DESCRIPTION_BYTES: usize = 4_096;
204pub(crate) const MAX_REGEX_BYTES: usize = 65_536;
205
206pub(crate) fn validate_identifier(kind: &'static str, value: &str) -> AnalysisResult<()> {
207 if value.len() > MAX_IDENTIFIER_BYTES {
208 return Err(AnalysisError::InputTooLarge {
209 field: kind,
210 actual: value.len(),
211 limit: MAX_IDENTIFIER_BYTES,
212 });
213 }
214
215 let reason = if value.trim().is_empty() {
216 Some("must not be empty or whitespace-only")
217 } else if value.chars().any(char::is_control) {
218 Some("must not contain control characters")
219 } else {
220 None
221 };
222
223 if let Some(reason) = reason {
224 return Err(AnalysisError::InvalidIdentifier {
225 kind,
226 name: value.to_string(),
227 reason,
228 });
229 }
230
231 Ok(())
232}
233
234pub(crate) fn compact_string(value: String) -> String {
235 value.into_boxed_str().into_string()
236}
237
238pub(crate) fn regex_error_reason(error: ®ex::Error) -> &'static str {
239 match error {
240 regex::Error::Syntax(_) => "syntax error",
241 regex::Error::CompiledTooBig(_) => "compiled expression exceeds the size limit",
242 _ => "regular expression compilation failed",
243 }
244}