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.
- package/README.md +56 -0
- package/bin/create-claude-config.js +1 -0
- package/package.json +7 -2
- package/src/analytics-web/chats_mobile.html +17 -16
- package/src/console-bridge.js +3 -3
- package/src/index.js +183 -9
- package/src/security-audit.js +164 -0
- package/src/validation/ARCHITECTURE.md +309 -0
- package/src/validation/BaseValidator.js +152 -0
- package/src/validation/README.md +543 -0
- package/src/validation/ValidationOrchestrator.js +305 -0
- package/src/validation/validators/IntegrityValidator.js +338 -0
- package/src/validation/validators/ProvenanceValidator.js +399 -0
- package/src/validation/validators/ReferenceValidator.js +373 -0
- package/src/validation/validators/SemanticValidator.js +449 -0
- package/src/validation/validators/StructuralValidator.js +376 -0
|
@@ -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: 
|
|
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;
|