Skip to main content

mockforge_intelligence/threat_modeling/
error_analyzer.rs

1//! Error response analysis
2//!
3//! This module analyzes error responses for security issues like:
4//! - Stack trace leakage
5//! - Database error messages
6//! - Internal paths/URLs
7//! - Sensitive configuration
8
9use super::types::{ThreatCategory, ThreatFinding, ThreatLevel};
10use mockforge_openapi::OpenApiSpec;
11use openapiv3::ReferenceOr;
12use std::collections::HashMap;
13
14/// Error analyzer for detecting error message leakage
15pub struct ErrorAnalyzer {
16    /// Whether error leakage detection is enabled
17    enabled: bool,
18}
19
20impl ErrorAnalyzer {
21    /// Create a new error analyzer
22    pub fn new(enabled: bool) -> Self {
23        Self { enabled }
24    }
25
26    /// Analyze error responses for leakage
27    pub fn analyze_errors(&self, spec: &OpenApiSpec) -> Vec<ThreatFinding> {
28        if !self.enabled {
29            return Vec::new();
30        }
31
32        let mut findings = Vec::new();
33
34        for (path, path_item) in &spec.spec.paths.paths {
35            if let ReferenceOr::Item(path_item) = path_item {
36                // Iterate over all HTTP methods
37                let methods = vec![
38                    ("GET", path_item.get.as_ref()),
39                    ("POST", path_item.post.as_ref()),
40                    ("PUT", path_item.put.as_ref()),
41                    ("DELETE", path_item.delete.as_ref()),
42                    ("PATCH", path_item.patch.as_ref()),
43                    ("HEAD", path_item.head.as_ref()),
44                    ("OPTIONS", path_item.options.as_ref()),
45                    ("TRACE", path_item.trace.as_ref()),
46                ];
47
48                for (method, operation_opt) in methods {
49                    let Some(operation) = operation_opt else {
50                        continue;
51                    };
52                    let base_path = format!("{}.{}", method, path);
53
54                    // Check error responses (4xx, 5xx)
55                    for (status_code, response) in &operation.responses.responses {
56                        let status_num = match status_code {
57                            openapiv3::StatusCode::Code(code) => *code,
58                            openapiv3::StatusCode::Range(range) => {
59                                // Range is a u16: 2 = 2XX, 3 = 3XX, 4 = 4XX, 5 = 5XX
60                                match *range {
61                                    4 => 400,
62                                    5 => 500,
63                                    _ => continue,
64                                }
65                            }
66                        };
67
68                        // Focus on error status codes
69                        if status_num >= 400 {
70                            if let ReferenceOr::Item(resp) = response {
71                                for (_content_type, media_type) in &resp.content {
72                                    if let Some(schema) = &media_type.schema {
73                                        findings.extend(
74                                            self.analyze_error_schema(
75                                                schema, &base_path, status_num,
76                                            ),
77                                        );
78                                    }
79
80                                    // Check examples
81                                    for example in media_type.examples.values() {
82                                        if let ReferenceOr::Item(example_item) = example {
83                                            if let Some(example_value) = &example_item.value {
84                                                findings.extend(self.analyze_error_example(
85                                                    example_value,
86                                                    &base_path,
87                                                    status_num,
88                                                ));
89                                            }
90                                        }
91                                    }
92                                }
93                            }
94                        }
95                    }
96                }
97            }
98        }
99
100        findings
101    }
102
103    /// Analyze error schema for leakage patterns
104    fn analyze_error_schema(
105        &self,
106        schema_ref: &ReferenceOr<openapiv3::Schema>,
107        base_path: &str,
108        status_code: u16,
109    ) -> Vec<ThreatFinding> {
110        let mut findings = Vec::new();
111
112        if let ReferenceOr::Item(schema) = schema_ref {
113            // Check description for stack trace keywords
114            if let Some(description) = &schema.schema_data.description {
115                if self.contains_stack_trace_keywords(description) {
116                    findings.push(ThreatFinding {
117                        finding_type: ThreatCategory::StackTraceLeakage,
118                        severity: ThreatLevel::High,
119                        description:
120                            "Error response schema description suggests stack trace exposure"
121                                .to_string(),
122                        field_path: Some(format!("{}.error.{}", base_path, status_code)),
123                        context: HashMap::new(),
124                        confidence: 0.8,
125                    });
126                }
127            }
128
129            // Check properties for sensitive fields
130            if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj_type)) =
131                &schema.schema_kind
132            {
133                for (prop_name, _) in &obj_type.properties {
134                    let prop_lower = prop_name.to_lowercase();
135                    if prop_lower.contains("stack")
136                        || prop_lower.contains("trace")
137                        || prop_lower.contains("exception")
138                        || prop_lower.contains("error_detail")
139                    {
140                        findings.push(ThreatFinding {
141                            finding_type: ThreatCategory::StackTraceLeakage,
142                            severity: ThreatLevel::Critical,
143                            description: format!(
144                                "Error response contains '{}' field which may leak stack traces",
145                                prop_name
146                            ),
147                            field_path: Some(format!(
148                                "{}.error.{}.{}",
149                                base_path, status_code, prop_name
150                            )),
151                            context: HashMap::new(),
152                            confidence: 0.9,
153                        });
154                    }
155                }
156            }
157        }
158
159        findings
160    }
161
162    /// Analyze error example for leakage
163    fn analyze_error_example(
164        &self,
165        example: &serde_json::Value,
166        base_path: &str,
167        status_code: u16,
168    ) -> Vec<ThreatFinding> {
169        let mut findings = Vec::new();
170
171        if let Some(obj) = example.as_object() {
172            for (key, value) in obj {
173                // Check for stack traces in values
174                if let Some(str_value) = value.as_str() {
175                    if self.contains_stack_trace_patterns(str_value) {
176                        findings.push(ThreatFinding {
177                            finding_type: ThreatCategory::StackTraceLeakage,
178                            severity: ThreatLevel::Critical,
179                            description:
180                                "Error example contains stack trace or sensitive error details"
181                                    .to_string(),
182                            field_path: Some(format!(
183                                "{}.error.{}.{}",
184                                base_path, status_code, key
185                            )),
186                            context: HashMap::new(),
187                            confidence: 1.0,
188                        });
189                    }
190
191                    // Check for file paths
192                    if str_value.contains("/")
193                        && (str_value.contains(".py")
194                            || str_value.contains(".java")
195                            || str_value.contains(".rs"))
196                    {
197                        findings.push(ThreatFinding {
198                            finding_type: ThreatCategory::ErrorLeakage,
199                            severity: ThreatLevel::Medium,
200                            description:
201                                "Error message contains file path which may leak internal structure"
202                                    .to_string(),
203                            field_path: Some(format!(
204                                "{}.error.{}.{}",
205                                base_path, status_code, key
206                            )),
207                            context: HashMap::new(),
208                            confidence: 0.7,
209                        });
210                    }
211                }
212            }
213        }
214
215        findings
216    }
217
218    /// Check if text contains stack trace keywords
219    fn contains_stack_trace_keywords(&self, text: &str) -> bool {
220        let text_lower = text.to_lowercase();
221        text_lower.contains("stack trace")
222            || text_lower.contains("stacktrace")
223            || text_lower.contains("exception")
224            || text_lower.contains("traceback")
225    }
226
227    /// Check if text contains stack trace patterns
228    fn contains_stack_trace_patterns(&self, text: &str) -> bool {
229        // Look for common stack trace patterns
230        text.contains("at ") && (text.contains("(") || text.contains("line"))
231            || text.contains("Traceback")
232            || text.contains("Exception in thread")
233    }
234}
235
236impl Default for ErrorAnalyzer {
237    fn default() -> Self {
238        Self::new(true)
239    }
240}