Skip to main content

gatekpr_patterns/
webhooks.rs

1//! Webhook-related patterns for Shopify validation
2//!
3//! Includes patterns for GDPR webhooks, mandatory webhooks, and webhook security.
4
5use crate::registry::PatternRegistry;
6use once_cell::sync::Lazy;
7
8/// Pre-built webhook pattern registry
9pub static WEBHOOK_PATTERNS: Lazy<PatternRegistry> = Lazy::new(|| {
10    let mut registry = PatternRegistry::new();
11
12    // GDPR mandatory webhooks
13    registry
14        .register(
15            "gdpr_data_request",
16            r"(?i)(customers[/_]data[/_]request|CUSTOMERS_DATA_REQUEST|customersDataRequest|data[-_]request)",
17        )
18        .unwrap();
19
20    registry
21        .register(
22            "gdpr_customers_redact",
23            r"(?i)(customers[/_]redact|CUSTOMERS_REDACT|customersRedact|customer[-_]redact)",
24        )
25        .unwrap();
26
27    registry
28        .register(
29            "gdpr_shop_redact",
30            r"(?i)(shop[/_]redact|SHOP_REDACT|shopRedact|shop[-_]redact)",
31        )
32        .unwrap();
33
34    // HMAC verification patterns
35    registry
36        .register(
37            "hmac_verification",
38            r"(?i)(X-Shopify-Hmac-SHA256|hmac|verify.*hmac|validateSignature|verifyWebhook|hmac_sha256)",
39        )
40        .unwrap();
41
42    // Webhook endpoint patterns
43    registry
44        .register(
45            "webhook_endpoint",
46            r"(?i)(/webhooks?/|webhook.*handler|handle.*webhook|@webhook)",
47        )
48        .unwrap();
49
50    // Webhook subscription
51    registry
52        .register(
53            "webhook_subscription",
54            r"(?i)(webhookSubscription|subscribe.*webhook|createWebhook|registerWebhook)",
55        )
56        .unwrap();
57
58    // App uninstall webhook
59    registry
60        .register(
61            "app_uninstall",
62            r"(?i)(app[/_]uninstalled?|APP_UNINSTALLED|appUninstalled|uninstall.*webhook)",
63        )
64        .unwrap();
65
66    // Shop update webhook
67    registry
68        .register(
69            "shop_update",
70            r"(?i)(shop[/_]update|SHOP_UPDATE|shopUpdate)",
71        )
72        .unwrap();
73
74    registry
75});
76
77/// Pattern keys for GDPR webhooks
78pub const GDPR_WEBHOOK_KEYS: &[&str] = &[
79    "gdpr_data_request",
80    "gdpr_customers_redact",
81    "gdpr_shop_redact",
82];
83
84/// Pattern keys for webhook security
85pub const WEBHOOK_SECURITY_KEYS: &[&str] = &["hmac_verification"];
86
87/// Check if text contains any GDPR webhook pattern
88pub fn has_gdpr_webhooks(text: &str) -> bool {
89    WEBHOOK_PATTERNS.any_match(GDPR_WEBHOOK_KEYS, text)
90}
91
92/// Check which GDPR webhooks are present
93pub fn check_gdpr_webhooks(text: &str) -> GdprWebhookStatus {
94    GdprWebhookStatus {
95        data_request: WEBHOOK_PATTERNS.is_match("gdpr_data_request", text),
96        customers_redact: WEBHOOK_PATTERNS.is_match("gdpr_customers_redact", text),
97        shop_redact: WEBHOOK_PATTERNS.is_match("gdpr_shop_redact", text),
98        hmac_verification: WEBHOOK_PATTERNS.is_match("hmac_verification", text),
99    }
100}
101
102/// Status of GDPR webhook implementation
103#[derive(Debug, Clone, Default)]
104pub struct GdprWebhookStatus {
105    pub data_request: bool,
106    pub customers_redact: bool,
107    pub shop_redact: bool,
108    pub hmac_verification: bool,
109}
110
111impl GdprWebhookStatus {
112    /// Check if all required GDPR webhooks are implemented
113    pub fn is_compliant(&self) -> bool {
114        self.data_request && self.customers_redact && self.shop_redact
115    }
116
117    /// Check if HMAC verification is present
118    pub fn has_security(&self) -> bool {
119        self.hmac_verification
120    }
121
122    /// Get list of missing webhooks
123    pub fn missing(&self) -> Vec<&'static str> {
124        let mut missing = Vec::new();
125        if !self.data_request {
126            missing.push("customers/data_request");
127        }
128        if !self.customers_redact {
129            missing.push("customers/redact");
130        }
131        if !self.shop_redact {
132            missing.push("shop/redact");
133        }
134        missing
135    }
136
137    /// Get list of implemented webhooks
138    pub fn implemented(&self) -> Vec<&'static str> {
139        let mut implemented = Vec::new();
140        if self.data_request {
141            implemented.push("customers/data_request");
142        }
143        if self.customers_redact {
144            implemented.push("customers/redact");
145        }
146        if self.shop_redact {
147            implemented.push("shop/redact");
148        }
149        implemented
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_gdpr_webhook_detection() {
159        let code = r#"
160            app.post('/webhooks/customers/data_request', handleDataRequest);
161            app.post('/webhooks/customers/redact', handleRedact);
162            app.post('/webhooks/shop/redact', handleShopRedact);
163        "#;
164
165        let status = check_gdpr_webhooks(code);
166        assert!(status.is_compliant());
167    }
168
169    #[test]
170    fn test_missing_webhooks() {
171        let code = r#"
172            app.post('/webhooks/customers/data_request', handleDataRequest);
173        "#;
174
175        let status = check_gdpr_webhooks(code);
176        assert!(!status.is_compliant());
177        assert_eq!(status.missing(), vec!["customers/redact", "shop/redact"]);
178    }
179
180    #[test]
181    fn test_hmac_verification() {
182        let code = r#"
183            const hmac = request.headers['X-Shopify-Hmac-SHA256'];
184            verifyHmac(hmac, body);
185        "#;
186
187        let status = check_gdpr_webhooks(code);
188        assert!(status.has_security());
189    }
190}