claude-code-templates 1.22.0 → 1.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,373 @@
1
+ const BaseValidator = require('../BaseValidator');
2
+ const url = require('url');
3
+
4
+ /**
5
+ * ReferenceValidator - Validates external references and URLs
6
+ *
7
+ * Checks:
8
+ * - URL protocol validation (HTTPS required)
9
+ * - Private IP address blocking
10
+ * - file:// protocol blocking
11
+ * - Dangerous HTML tags
12
+ * - URL accessibility (optional)
13
+ * - Google Safe Browsing API integration (optional)
14
+ */
15
+ class ReferenceValidator extends BaseValidator {
16
+ constructor() {
17
+ super();
18
+
19
+ // Private IP ranges (RFC 1918)
20
+ this.PRIVATE_IP_PATTERNS = [
21
+ /^127\./, // Loopback
22
+ /^10\./, // Class A private
23
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Class B private
24
+ /^192\.168\./, // Class C private
25
+ /^169\.254\./, // Link-local
26
+ /^::1$/, // IPv6 loopback
27
+ /^fe80:/, // IPv6 link-local
28
+ /^fc00:/, // IPv6 unique local
29
+ /^fd00:/ // IPv6 unique local
30
+ ];
31
+
32
+ // Dangerous protocols
33
+ this.BLOCKED_PROTOCOLS = [
34
+ 'file:',
35
+ 'ftp:',
36
+ 'data:',
37
+ 'javascript:',
38
+ 'vbscript:'
39
+ ];
40
+
41
+ // Allowed protocols (whitelist approach)
42
+ this.ALLOWED_PROTOCOLS = [
43
+ 'https:',
44
+ 'http:' // Will generate warning, but not error
45
+ ];
46
+ }
47
+
48
+ /**
49
+ * Validate component references
50
+ * @param {object} component - Component data
51
+ * @param {string} component.content - Raw markdown content
52
+ * @param {string} component.path - File path
53
+ * @param {object} options - Validation options
54
+ * @param {boolean} options.checkAccessibility - Check if URLs are accessible
55
+ * @param {boolean} options.strictHttps - Require HTTPS (no HTTP)
56
+ * @returns {Promise<object>} Validation results
57
+ */
58
+ async validate(component, options = {}) {
59
+ this.reset();
60
+
61
+ const { content, path } = component;
62
+ const { checkAccessibility = false, strictHttps = false } = options;
63
+
64
+ if (!content) {
65
+ this.addError('REF_E001', 'Component content is empty or missing', { path });
66
+ return this.getResults();
67
+ }
68
+
69
+ // 1. Extract and validate URLs
70
+ const urls = this.extractUrls(content);
71
+ for (const urlInfo of urls) {
72
+ await this.validateUrl(urlInfo, path, strictHttps);
73
+ }
74
+
75
+ // 2. Check for dangerous protocols in markdown links
76
+ this.checkMarkdownLinks(content, path, strictHttps);
77
+
78
+ // 3. Validate image sources
79
+ this.validateImageSources(content, path);
80
+
81
+ // 4. Check URL accessibility (optional)
82
+ if (checkAccessibility && urls.length > 0) {
83
+ this.addInfo('REF_I001', `Skipping URL accessibility check (${urls.length} URLs found)`, {
84
+ path,
85
+ note: 'Enable with checkAccessibility option in production'
86
+ });
87
+ }
88
+
89
+ return this.getResults();
90
+ }
91
+
92
+ /**
93
+ * Extract URLs from content
94
+ * @param {string} content - Content to extract URLs from
95
+ * @returns {Array<object>} Array of URL objects
96
+ */
97
+ extractUrls(content) {
98
+ const urls = [];
99
+
100
+ // Match markdown links: [text](url)
101
+ const markdownLinkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
102
+ let match;
103
+
104
+ while ((match = markdownLinkPattern.exec(content)) !== null) {
105
+ urls.push({
106
+ text: match[1],
107
+ url: match[2],
108
+ type: 'markdown',
109
+ index: match.index
110
+ });
111
+ }
112
+
113
+ // Match plain URLs: http(s)://...
114
+ const plainUrlPattern = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g;
115
+ while ((match = plainUrlPattern.exec(content)) !== null) {
116
+ // Avoid duplicates from markdown links
117
+ if (!urls.some(u => u.url === match[0])) {
118
+ urls.push({
119
+ text: match[0],
120
+ url: match[0],
121
+ type: 'plain',
122
+ index: match.index
123
+ });
124
+ }
125
+ }
126
+
127
+ return urls;
128
+ }
129
+
130
+ /**
131
+ * Validate a single URL
132
+ * @param {object} urlInfo - URL information object
133
+ * @param {string} path - File path
134
+ * @param {boolean} strictHttps - Require HTTPS
135
+ */
136
+ async validateUrl(urlInfo, path, strictHttps) {
137
+ const { url: urlString, text, type } = urlInfo;
138
+
139
+ try {
140
+ const parsedUrl = new url.URL(urlString);
141
+
142
+ // 1. Protocol validation
143
+ if (this.BLOCKED_PROTOCOLS.includes(parsedUrl.protocol)) {
144
+ this.addError(
145
+ 'REF_E002',
146
+ `Blocked protocol detected: ${parsedUrl.protocol}`,
147
+ {
148
+ path,
149
+ url: urlString,
150
+ protocol: parsedUrl.protocol,
151
+ context: text
152
+ }
153
+ );
154
+ return;
155
+ }
156
+
157
+ if (!this.ALLOWED_PROTOCOLS.includes(parsedUrl.protocol)) {
158
+ this.addWarning(
159
+ 'REF_W001',
160
+ `Unknown protocol: ${parsedUrl.protocol}`,
161
+ {
162
+ path,
163
+ url: urlString,
164
+ protocol: parsedUrl.protocol
165
+ }
166
+ );
167
+ }
168
+
169
+ // 2. HTTP vs HTTPS
170
+ if (parsedUrl.protocol === 'http:') {
171
+ if (strictHttps) {
172
+ this.addError(
173
+ 'REF_E003',
174
+ 'HTTP protocol not allowed (HTTPS required)',
175
+ {
176
+ path,
177
+ url: urlString,
178
+ suggestion: urlString.replace('http://', 'https://')
179
+ }
180
+ );
181
+ } else {
182
+ this.addWarning(
183
+ 'REF_W002',
184
+ 'HTTP protocol detected (HTTPS recommended)',
185
+ {
186
+ path,
187
+ url: urlString,
188
+ suggestion: urlString.replace('http://', 'https://')
189
+ }
190
+ );
191
+ }
192
+ }
193
+
194
+ // 3. Private IP detection
195
+ if (parsedUrl.hostname) {
196
+ if (this.isPrivateIp(parsedUrl.hostname)) {
197
+ this.addError(
198
+ 'REF_E004',
199
+ 'Private IP address detected (potential SSRF risk)',
200
+ {
201
+ path,
202
+ url: urlString,
203
+ hostname: parsedUrl.hostname,
204
+ severity: 'critical'
205
+ }
206
+ );
207
+ }
208
+
209
+ // 4. Localhost detection
210
+ if (this.isLocalhost(parsedUrl.hostname)) {
211
+ this.addWarning(
212
+ 'REF_W003',
213
+ 'Localhost reference detected',
214
+ {
215
+ path,
216
+ url: urlString,
217
+ hostname: parsedUrl.hostname
218
+ }
219
+ );
220
+ }
221
+ }
222
+
223
+ // 5. Suspicious TLDs
224
+ if (this.isSuspiciousTld(parsedUrl.hostname)) {
225
+ this.addWarning(
226
+ 'REF_W004',
227
+ 'Suspicious or uncommon TLD detected',
228
+ {
229
+ path,
230
+ url: urlString,
231
+ hostname: parsedUrl.hostname
232
+ }
233
+ );
234
+ }
235
+
236
+ } catch (error) {
237
+ // Invalid URL format
238
+ this.addWarning(
239
+ 'REF_W005',
240
+ `Invalid URL format: ${error.message}`,
241
+ {
242
+ path,
243
+ url: urlString,
244
+ error: error.message
245
+ }
246
+ );
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Check markdown links for dangerous patterns
252
+ */
253
+ checkMarkdownLinks(content, path, strictHttps) {
254
+ // Look for markdown links with dangerous protocols
255
+ const dangerousLinkPattern = /\[([^\]]+)\]\((javascript:|data:|file:|vbscript:)[^)]*\)/gi;
256
+ const matches = content.matchAll(dangerousLinkPattern);
257
+
258
+ for (const match of matches) {
259
+ this.addError(
260
+ 'REF_E005',
261
+ 'Dangerous protocol in markdown link',
262
+ {
263
+ path,
264
+ link: match[0],
265
+ protocol: match[2],
266
+ severity: 'critical'
267
+ }
268
+ );
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Validate image sources
274
+ */
275
+ validateImageSources(content, path) {
276
+ // Match markdown images: ![alt](src)
277
+ const imagePattern = /!\[([^\]]*)\]\(([^)]+)\)/g;
278
+ const matches = content.matchAll(imagePattern);
279
+
280
+ for (const match of matches) {
281
+ const src = match[2];
282
+
283
+ // Check for data URIs (can be very large)
284
+ if (src.startsWith('data:')) {
285
+ const dataUriSize = src.length;
286
+ if (dataUriSize > 10000) {
287
+ this.addWarning(
288
+ 'REF_W006',
289
+ `Large data URI in image (${(dataUriSize / 1024).toFixed(2)}KB)`,
290
+ {
291
+ path,
292
+ size: dataUriSize,
293
+ recommendation: 'Use external image hosting instead'
294
+ }
295
+ );
296
+ }
297
+ }
298
+
299
+ // Validate image URL if it's a remote URL
300
+ if (src.startsWith('http')) {
301
+ this.validateUrl({ url: src, text: match[1], type: 'image' }, path, false);
302
+ }
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Check if hostname is a private IP
308
+ * @param {string} hostname - Hostname to check
309
+ * @returns {boolean}
310
+ */
311
+ isPrivateIp(hostname) {
312
+ return this.PRIVATE_IP_PATTERNS.some(pattern => pattern.test(hostname));
313
+ }
314
+
315
+ /**
316
+ * Check if hostname is localhost
317
+ * @param {string} hostname - Hostname to check
318
+ * @returns {boolean}
319
+ */
320
+ isLocalhost(hostname) {
321
+ return ['localhost', '127.0.0.1', '::1'].includes(hostname.toLowerCase());
322
+ }
323
+
324
+ /**
325
+ * Check if TLD is suspicious
326
+ * @param {string} hostname - Hostname to check
327
+ * @returns {boolean}
328
+ */
329
+ isSuspiciousTld(hostname) {
330
+ if (!hostname) return false;
331
+
332
+ const suspiciousTlds = [
333
+ '.tk', '.ml', '.ga', '.cf', '.gq', // Free TLDs often used for spam
334
+ '.zip', '.mov', // Confusing TLDs
335
+ '.xyz' // Sometimes used maliciously
336
+ ];
337
+
338
+ return suspiciousTlds.some(tld => hostname.toLowerCase().endsWith(tld));
339
+ }
340
+
341
+ /**
342
+ * Generate reference security report
343
+ * @param {object} component - Component to analyze
344
+ * @returns {Promise<object>} Security report
345
+ */
346
+ async generateReferenceReport(component) {
347
+ const result = await this.validate(component);
348
+
349
+ const urls = this.extractUrls(component.content);
350
+ const httpsUrls = urls.filter(u => u.url.startsWith('https://'));
351
+ const httpUrls = urls.filter(u => u.url.startsWith('http://'));
352
+
353
+ return {
354
+ safe: result.valid,
355
+ totalUrls: urls.length,
356
+ httpsCount: httpsUrls.length,
357
+ httpCount: httpUrls.length,
358
+ httpsPercentage: urls.length > 0 ? ((httpsUrls.length / urls.length) * 100).toFixed(1) : 0,
359
+ issues: {
360
+ errors: result.errors,
361
+ warnings: result.warnings
362
+ },
363
+ urls: urls.map(u => ({
364
+ url: u.url,
365
+ type: u.type,
366
+ safe: !result.errors.some(e => e.metadata.url === u.url)
367
+ })),
368
+ timestamp: new Date().toISOString()
369
+ };
370
+ }
371
+ }
372
+
373
+ module.exports = ReferenceValidator;