gatekpr_patterns/
webhooks.rs1use crate::registry::PatternRegistry;
6use once_cell::sync::Lazy;
7
8pub static WEBHOOK_PATTERNS: Lazy<PatternRegistry> = Lazy::new(|| {
10 let mut registry = PatternRegistry::new();
11
12 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 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 registry
44 .register(
45 "webhook_endpoint",
46 r"(?i)(/webhooks?/|webhook.*handler|handle.*webhook|@webhook)",
47 )
48 .unwrap();
49
50 registry
52 .register(
53 "webhook_subscription",
54 r"(?i)(webhookSubscription|subscribe.*webhook|createWebhook|registerWebhook)",
55 )
56 .unwrap();
57
58 registry
60 .register(
61 "app_uninstall",
62 r"(?i)(app[/_]uninstalled?|APP_UNINSTALLED|appUninstalled|uninstall.*webhook)",
63 )
64 .unwrap();
65
66 registry
68 .register(
69 "shop_update",
70 r"(?i)(shop[/_]update|SHOP_UPDATE|shopUpdate)",
71 )
72 .unwrap();
73
74 registry
75});
76
77pub const GDPR_WEBHOOK_KEYS: &[&str] = &[
79 "gdpr_data_request",
80 "gdpr_customers_redact",
81 "gdpr_shop_redact",
82];
83
84pub const WEBHOOK_SECURITY_KEYS: &[&str] = &["hmac_verification"];
86
87pub fn has_gdpr_webhooks(text: &str) -> bool {
89 WEBHOOK_PATTERNS.any_match(GDPR_WEBHOOK_KEYS, text)
90}
91
92pub 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#[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 pub fn is_compliant(&self) -> bool {
114 self.data_request && self.customers_redact && self.shop_redact
115 }
116
117 pub fn has_security(&self) -> bool {
119 self.hmac_verification
120 }
121
122 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 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}