dn-scribe commited on
Commit
cbcecd2
·
1 Parent(s): 0579ed2

functioning templating (metadata)

Browse files
api/webhooks.py CHANGED
@@ -1,3 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  def trigger_webhook(webhook, attestation_data, event=None):
2
  """
3
  Send a webhook with template support for headers and content.
@@ -7,73 +23,58 @@ def trigger_webhook(webhook, attestation_data, event=None):
7
  import json, requests
8
  # headers = {'Content-Type': 'application/json'}
9
  headers = {}
 
 
10
  if webhook.headers:
11
  try:
12
  user_headers = json.loads(webhook.headers)
13
  for k, v in user_headers.items():
14
- if isinstance(v, str) and '{secret}' in v:
15
- user_headers[k] = v.replace('{secret}', webhook.secret or '')
16
  headers.update(user_headers)
17
  except Exception as e:
18
  current_app.logger.error(f"Error parsing webhook headers: {e}")
19
  return None
20
- if webhook.secret and not any('{secret}' in str(v) for v in (json.loads(webhook.headers).values() if webhook.headers else [])):
 
21
  headers['X-Webhook-Secret'] = webhook.secret
22
- # Prepare content
23
  if webhook.content:
24
  try:
25
  content_str = webhook.content
26
- # Replace {attestation_details} with the Python dict (not JSON string)
27
- if '{attestation_details}' in content_str:
28
- # Safely replace the string with the dict in the parsed object
29
- # Parse as JSON, then recursively replace the value
30
  obj = json.loads(content_str)
31
- def replace_attestation_details(o):
32
- if isinstance(o, dict):
33
- return {k: (attestation_data if v == '{attestation_details}' else replace_attestation_details(v)) for k, v in o.items()}
34
- elif isinstance(o, list):
35
- return [replace_attestation_details(i) for i in o]
36
- elif isinstance(o, str):
37
- if '{event}' in o and event is not None:
38
- o = o.replace('{event}', str(event))
39
- if '{webhook_id}' in o:
40
- o = o.replace('{webhook_id}', str(getattr(webhook, 'id', '')))
41
- return o
42
- else:
43
- return o
44
- payload = replace_attestation_details(obj)
45
- else:
46
- # No {attestation_details}, just replace other fields
47
- content_str = content_str
48
- if event is not None:
49
- content_str = content_str.replace('{event}', str(event))
50
- content_str = content_str.replace('{webhook_id}', str(getattr(webhook, 'id', '')))
51
- payload = json.loads(content_str)
52
  except Exception as e:
53
  current_app.logger.error(f"Error parsing webhook content: {e}")
54
  return None
55
  else:
56
  payload = {'attestation': attestation_data}
57
- # if event:
58
- # payload['event'] = event
59
- # payload['webhook_id'] = webhook.id
60
  method = webhook.method or 'POST'
61
  try:
62
  req_func = getattr(requests, method.lower(), requests.post)
63
  # Log sending attempt
64
  log_event(
65
  severity=6, # Informational
66
- message=f"Sending webhook to {webhook.url} with payload: {payload}",
67
  event_type=event or getattr(webhook, 'event_type', None),
68
  user_id=getattr(webhook, 'created_by', None),
69
  source_ip=None,
70
  app_name='webhook-sender'
71
  )
72
- resp = req_func(webhook.url, data=json.dumps(payload), headers=headers, timeout=5)
73
  # Log response code
74
  log_event(
75
  severity=6,
76
- message=f"Webhook sent to {webhook.url}, response code: {resp.status_code}",
77
  event_type=event or getattr(webhook, 'event_type', None),
78
  user_id=getattr(webhook, 'created_by', None),
79
  source_ip=None,
@@ -83,7 +84,7 @@ def trigger_webhook(webhook, attestation_data, event=None):
83
  except Exception as e:
84
  log_event(
85
  severity=3, # Error
86
- message=f"Error sending webhook to {webhook.url}: {e}",
87
  event_type=event or getattr(webhook, 'event_type', None),
88
  user_id=getattr(webhook, 'created_by', None),
89
  source_ip=None,
@@ -91,17 +92,116 @@ def trigger_webhook(webhook, attestation_data, event=None):
91
  )
92
  return None
93
 
94
- import json
95
- import io
96
- from flask import Blueprint, request, jsonify, send_file
97
- from flask_jwt_extended import get_jwt_identity
98
- from models import Webhook, User, Attestation
99
- from app import db
100
- from auth import jwt_required_with_logging
101
- import requests
102
- from datetime import datetime
103
- from logging_utils import log_event
104
- from flask import current_app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  webhooks_bp = Blueprint('webhooks', __name__)
107
 
 
1
+
2
+ import json
3
+ import io
4
+ from flask import Blueprint, request, jsonify, send_file
5
+ from flask_jwt_extended import get_jwt_identity
6
+ from models import Webhook, User, Attestation
7
+ from app import db
8
+ from auth import jwt_required_with_logging
9
+ import requests
10
+ from datetime import datetime
11
+ from logging_utils import log_event
12
+ from flask import current_app
13
+
14
+ import re
15
+ from storage import get_storage_backend
16
+
17
  def trigger_webhook(webhook, attestation_data, event=None):
18
  """
19
  Send a webhook with template support for headers and content.
 
23
  import json, requests
24
  # headers = {'Content-Type': 'application/json'}
25
  headers = {}
26
+ context = attestation_data.get("context", {}) if isinstance(attestation_data, dict) else {}
27
+ # Render headers with templating
28
  if webhook.headers:
29
  try:
30
  user_headers = json.loads(webhook.headers)
31
  for k, v in user_headers.items():
32
+ user_headers[k] = render_template_string(v, context, attestation_data, webhook)
 
33
  headers.update(user_headers)
34
  except Exception as e:
35
  current_app.logger.error(f"Error parsing webhook headers: {e}")
36
  return None
37
+ # Render secret header if not templated
38
+ if webhook.secret and not any("--secret--" in str(v) for v in (json.loads(webhook.headers).values() if webhook.headers else [])):
39
  headers['X-Webhook-Secret'] = webhook.secret
40
+ # Prepare content with templating
41
  if webhook.content:
42
  try:
43
  content_str = webhook.content
44
+ # Try to parse as JSON, else treat as string
45
+ try:
 
 
46
  obj = json.loads(content_str)
47
+ payload = render_template_string(obj, context, attestation_data, webhook)
48
+ except Exception:
49
+ payload = render_template_string(content_str, context, attestation_data, webhook)
50
+ try:
51
+ payload = json.loads(payload)
52
+ except Exception:
53
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  except Exception as e:
55
  current_app.logger.error(f"Error parsing webhook content: {e}")
56
  return None
57
  else:
58
  payload = {'attestation': attestation_data}
59
+ # Render URL with templating
60
+ url = render_template_string(webhook.url, context, attestation_data, webhook)
 
61
  method = webhook.method or 'POST'
62
  try:
63
  req_func = getattr(requests, method.lower(), requests.post)
64
  # Log sending attempt
65
  log_event(
66
  severity=6, # Informational
67
+ message=f"Sending webhook to {url} with payload: {payload}",
68
  event_type=event or getattr(webhook, 'event_type', None),
69
  user_id=getattr(webhook, 'created_by', None),
70
  source_ip=None,
71
  app_name='webhook-sender'
72
  )
73
+ resp = req_func(url, data=json.dumps(payload), headers=headers, timeout=5)
74
  # Log response code
75
  log_event(
76
  severity=6,
77
+ message=f"Webhook sent to {url}, response code: {resp.status_code}",
78
  event_type=event or getattr(webhook, 'event_type', None),
79
  user_id=getattr(webhook, 'created_by', None),
80
  source_ip=None,
 
84
  except Exception as e:
85
  log_event(
86
  severity=3, # Error
87
+ message=f"Error sending webhook to {url}: {e}",
88
  event_type=event or getattr(webhook, 'event_type', None),
89
  user_id=getattr(webhook, 'created_by', None),
90
  source_ip=None,
 
92
  )
93
  return None
94
 
95
+ def render_template_string(template, context=None, attestation=None, webhook=None):
96
+ """
97
+ Replace all "--varname--" patterns in the template string with their resolved values.
98
+ All replacements are done at the string level, then the result is parsed as JSON if possible.
99
+ """
100
+ import json
101
+
102
+ context = context or {}
103
+ attestation = attestation or {}
104
+ labels = context.get('labels', []) if isinstance(context.get('labels', []), list) else []
105
+ storage_path = attestation.get('storage_path')
106
+ storage_url = attestation.get('storage_url')
107
+ attestation_loaded = {}
108
+
109
+ def get_label_value(label_name):
110
+ for label in labels:
111
+ if isinstance(label, str) and label.startswith(f"{label_name}="):
112
+ return label.split("=", 1)[1]
113
+ return ""
114
+
115
+ def resolve_var(varname):
116
+ # Built-in
117
+ if varname == "secret":
118
+ return getattr(webhook, "secret", "") if webhook else ""
119
+ if varname == "product-key":
120
+ return attestation.get("product_key", "")
121
+ if varname == "product-version":
122
+ return attestation.get("product_version", "")
123
+ if varname == "attestation_details":
124
+ return attestation
125
+ # Attestation-derived (load only if needed)
126
+ if varname in ("raw_attestation", "attestation_json", "attestation_predicate", "attestation_predicate_b64"):
127
+ if "raw" not in attestation_loaded:
128
+ attestation_loaded["raw"] = None
129
+ if storage_path or storage_url:
130
+ backend = get_storage_backend()
131
+ try:
132
+ if storage_path:
133
+ attestation_loaded["raw"] = backend.download_file(storage_path)
134
+ elif storage_url:
135
+ import requests
136
+ resp = requests.get(storage_url)
137
+ if resp.ok:
138
+ attestation_loaded["raw"] = resp.content
139
+ except Exception:
140
+ attestation_loaded["raw"] = None
141
+ raw = attestation_loaded["raw"]
142
+ if raw is None:
143
+ return ""
144
+ if varname == "raw_attestation":
145
+ return raw
146
+ if varname == "attestation_json":
147
+ try:
148
+ return json.loads(raw)
149
+ except Exception:
150
+ return ""
151
+ if varname == "attestation_predicate":
152
+ try:
153
+ j = json.loads(raw)
154
+ return j.get("predicate", "")
155
+ except Exception:
156
+ return ""
157
+ if varname == "attestation_predicate_b64":
158
+ try:
159
+ import base64
160
+ j = json.loads(raw)
161
+ pred = j.get("predicate", "")
162
+ if isinstance(pred, (dict, list)):
163
+ pred = json.dumps(pred)
164
+ return base64.b64encode(pred.encode()).decode()
165
+ except Exception:
166
+ return ""
167
+ # Context fields
168
+ if varname.startswith("labels."):
169
+ label_name = varname.split(".", 1)[1]
170
+ return get_label_value(label_name)
171
+ if varname in context:
172
+ return str(context[varname])
173
+ # Not found
174
+ return ""
175
+
176
+ # Always operate on strings
177
+ if not isinstance(template, str):
178
+ return template
179
+
180
+ pattern = r'--([a-zA-Z0-9_.-]+)--'
181
+
182
+ def json_escape(val):
183
+ # For dict/list, dump as JSON; for bytes, decode; else str
184
+ if isinstance(val, (dict, list)):
185
+ return json.dumps(val)
186
+ if isinstance(val, bytes):
187
+ try:
188
+ return val.decode()
189
+ except Exception:
190
+ return str(val)
191
+ return str(val)
192
+
193
+ def replacer(match):
194
+ varname = match.group(1)
195
+ value = resolve_var(varname)
196
+ return json_escape(value)
197
+
198
+ replaced = re.sub(pattern, replacer, template)
199
+
200
+ # Try to parse as JSON, fallback to string
201
+ try:
202
+ return json.loads(replaced)
203
+ except Exception:
204
+ return replaced
205
 
206
  webhooks_bp = Blueprint('webhooks', __name__)
207
 
templates/webhook_form.html CHANGED
@@ -81,9 +81,20 @@
81
  <label for="content" class="form-label">Content Template (JSON)</label>
82
  <textarea class="form-control" id="content" name="content" rows="5" placeholder='{"key":"value", "attestation":{attestation_details}}'>{% if webhook and webhook.content %}{{ webhook.content }}{% else %}{"attestation":{attestation_details}}{% endif %}</textarea>
83
  <small class="form-text text-muted">
84
- Use <code>"{attestation_details}"</code> (with quotes) in your JSON template to inject the attestation dictionary.<br>
85
- You can also use <code>{event}</code> for the event type and <code>{webhook_id}</code> for the webhook ID in your template.<br>
86
- Example: <code>{"attestation": "{attestation_details}"}</code> will inject the attestation as a dictionary, not a string.
 
 
 
 
 
 
 
 
 
 
 
87
  </small>
88
  </div>
89
  <div class="mb-3">
 
81
  <label for="content" class="form-label">Content Template (JSON)</label>
82
  <textarea class="form-control" id="content" name="content" rows="5" placeholder='{"key":"value", "attestation":{attestation_details}}'>{% if webhook and webhook.content %}{{ webhook.content }}{% else %}{"attestation":{attestation_details}}{% endif %}</textarea>
83
  <small class="form-text text-muted">
84
+ <b>Templating:</b> You can use variables in the form <code>"--varname--"</code> (double quote, two dashes, name, two dashes, double quote) anywhere in the URL, headers, or content.<br>
85
+ <b>Supported variables:</b>
86
+ <ul>
87
+ <li><code>secret</code>, <code>product-key</code>, <code>product-version</code>, <code>attestation_details</code></li>
88
+ <li><code>raw_attestation</code>, <code>attestation_json</code>, <code>attestation_predicate</code>, <code>attestation_predicate_b64</code> (loads attestation from storage only if used)</li>
89
+ <li>Any context field: <code>--contextkey--</code> (empty string if missing)</li>
90
+ <li>Labels: <code>--labels.labelname--</code> (gets value for label <code>labelname</code>, empty if not found)</li>
91
+ </ul>
92
+ <b>Examples:</b>
93
+ <ul>
94
+ <li><code>{"Authorization": "Bearer \"--secret--\""}</code> → <code>{"Authorization": "Bearer ghpat-1232455"}</code></li>
95
+ <li><code>{"info": "--attestation_details--"}</code> → <code>{"info": {"id": 5, "context": {...}}}</code></li>
96
+ </ul>
97
+ If a variable is not found, it is replaced with an empty string.
98
  </small>
99
  </div>
100
  <div class="mb-3">
templates/webhooks.html CHANGED
@@ -180,9 +180,20 @@
180
  <label for="webhookContent" class="form-label">Content Template (JSON)</label>
181
  <textarea class="form-control" id="webhookContent" name="content" rows="5" placeholder='{"attestation":{attestation_details}}'></textarea>
182
  <small class="form-text text-muted">
183
- Use <code>"{attestation_details}"</code> (with quotes) in your JSON template to inject the attestation dictionary.<br>
184
- You can also use <code>{event}</code> for the event type and <code>{webhook_id}</code> for the webhook ID in your template.<br>
185
- Example: <code>{"attestation": "{attestation_details}"}</code> will inject the attestation as a dictionary, not a string.
 
 
 
 
 
 
 
 
 
 
 
186
  </small>
187
  </div>
188
  <div class="mb-3">
 
180
  <label for="webhookContent" class="form-label">Content Template (JSON)</label>
181
  <textarea class="form-control" id="webhookContent" name="content" rows="5" placeholder='{"attestation":{attestation_details}}'></textarea>
182
  <small class="form-text text-muted">
183
+ <b>Templating:</b> You can use variables in the form <code>"--varname--"</code> (double quote, two dashes, name, two dashes, double quote) anywhere in the URL, headers, or content.<br>
184
+ <b>Supported variables:</b>
185
+ <ul>
186
+ <li><code>secret</code>, <code>product-key</code>, <code>product-version</code>, <code>attestation_details</code></li>
187
+ <li><code>raw_attestation</code>, <code>attestation_json</code>, <code>attestation_predicate</code>, <code>attestation_predicate_b64</code> (loads attestation from storage only if used)</li>
188
+ <li>Any context field: <code>--contextkey--</code> (empty string if missing)</li>
189
+ <li>Labels: <code>--labels.labelname--</code> (gets value for label <code>labelname</code>, empty if not found)</li>
190
+ </ul>
191
+ <b>Examples:</b>
192
+ <ul>
193
+ <li><code>{"Authorization": "Bearer \"--secret--\""}</code> → <code>{"Authorization": "Bearer ghpat-1232455"}</code></li>
194
+ <li><code>{"info": "--attestation_details--"}</code> → <code>{"info": {"id": 5, "context": {...}}}</code></li>
195
+ </ul>
196
+ If a variable is not found, it is replaced with an empty string.
197
  </small>
198
  </div>
199
  <div class="mb-3">