Skip to main content

mockforge_intelligence/threat_modeling/
schema_analyzer.rs

1//! Schema design analysis
2//!
3//! This module analyzes schema design for security and consistency issues:
4//! - Too many optional fields
5//! - Inconsistent patterns
6//! - Missing validation
7
8use super::types::{ThreatCategory, ThreatFinding, ThreatLevel};
9use mockforge_openapi::OpenApiSpec;
10use openapiv3::ReferenceOr;
11use std::collections::HashMap;
12
13/// Schema analyzer for design issues
14pub struct SchemaAnalyzer {
15    /// Maximum optional fields threshold
16    max_optional_fields: usize,
17}
18
19impl SchemaAnalyzer {
20    /// Create a new schema analyzer
21    pub fn new(max_optional_fields: usize) -> Self {
22        Self {
23            max_optional_fields,
24        }
25    }
26
27    /// Analyze schemas for design issues
28    pub fn analyze_schemas(&self, spec: &OpenApiSpec) -> Vec<ThreatFinding> {
29        let mut findings = Vec::new();
30
31        for (path, path_item) in &spec.spec.paths.paths {
32            if let ReferenceOr::Item(path_item) = path_item {
33                // Iterate over all HTTP methods
34                let methods = vec![
35                    ("GET", path_item.get.as_ref()),
36                    ("POST", path_item.post.as_ref()),
37                    ("PUT", path_item.put.as_ref()),
38                    ("DELETE", path_item.delete.as_ref()),
39                    ("PATCH", path_item.patch.as_ref()),
40                    ("HEAD", path_item.head.as_ref()),
41                    ("OPTIONS", path_item.options.as_ref()),
42                    ("TRACE", path_item.trace.as_ref()),
43                ];
44
45                for (method, operation_opt) in methods {
46                    let Some(operation) = operation_opt else {
47                        continue;
48                    };
49                    let base_path = format!("{}.{}", method, path);
50
51                    // Analyze request body
52                    if let Some(request_body) = &operation.request_body {
53                        if let Some(ref_or_item) = request_body.as_item() {
54                            for media_type in ref_or_item.content.values() {
55                                if let Some(schema) = &media_type.schema {
56                                    findings.extend(
57                                        self.analyze_schema_design(schema, &base_path, "request"),
58                                    );
59                                }
60                            }
61                        }
62                    }
63
64                    // Analyze responses
65                    for (status_code, response) in &operation.responses.responses {
66                        if let ReferenceOr::Item(resp) = response {
67                            for media_type in resp.content.values() {
68                                if let Some(schema) = &media_type.schema {
69                                    findings.extend(self.analyze_schema_design(
70                                        schema,
71                                        &base_path,
72                                        &format!("response.{}", status_code),
73                                    ));
74                                }
75                            }
76                        }
77                    }
78                }
79            }
80        }
81
82        findings
83    }
84
85    /// Analyze schema design
86    fn analyze_schema_design(
87        &self,
88        schema_ref: &ReferenceOr<openapiv3::Schema>,
89        base_path: &str,
90        context: &str,
91    ) -> Vec<ThreatFinding> {
92        let mut findings = Vec::new();
93
94        if let ReferenceOr::Item(schema) = schema_ref {
95            if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj_type)) =
96                &schema.schema_kind
97            {
98                let required = obj_type.required.len();
99                let total_fields = obj_type.properties.len();
100                let optional_fields = total_fields.saturating_sub(required);
101
102                if optional_fields > self.max_optional_fields {
103                    findings.push(ThreatFinding {
104                        finding_type: ThreatCategory::ExcessiveOptionalFields,
105                        severity: ThreatLevel::Medium,
106                        description: format!(
107                            "Schema has {} optional fields (threshold: {}), which may indicate inconsistent backend behavior",
108                            optional_fields,
109                            self.max_optional_fields
110                        ),
111                        field_path: Some(format!("{}.{}", base_path, context)),
112                        context: HashMap::new(),
113                        confidence: 0.7,
114                    });
115                }
116
117                // Check for missing validation constraints
118                for (prop_name, prop_schema) in &obj_type.properties {
119                    if let ReferenceOr::Item(prop_schema_item) = prop_schema {
120                        // Check if it's a string type and has validation constraints
121                        if let openapiv3::SchemaKind::Type(openapiv3::Type::String(string_type)) =
122                            &prop_schema_item.as_ref().schema_kind
123                        {
124                            // Check if string has format, pattern, or maxLength
125                            let has_format = !matches!(
126                                string_type.format,
127                                openapiv3::VariantOrUnknownOrEmpty::Empty
128                            );
129                            let has_pattern = string_type.pattern.is_some();
130                            let has_max_length = string_type.max_length.is_some();
131
132                            if !has_format && !has_pattern && !has_max_length {
133                                findings.push(ThreatFinding {
134                                    finding_type: ThreatCategory::MissingValidation,
135                                    severity: ThreatLevel::Low,
136                                    description: format!(
137                                        "String field '{}' lacks validation constraints (format, pattern, or maxLength)",
138                                        prop_name
139                                    ),
140                                    field_path: Some(format!("{}.{}.{}", base_path, context, prop_name)),
141                                    context: HashMap::new(),
142                                    confidence: 0.6,
143                                });
144                            }
145                        }
146                    }
147                }
148            }
149        }
150
151        findings
152    }
153}
154
155impl Default for SchemaAnalyzer {
156    fn default() -> Self {
157        Self::new(10)
158    }
159}