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,305 @@
1
+ const StructuralValidator = require('./validators/StructuralValidator');
2
+ const IntegrityValidator = require('./validators/IntegrityValidator');
3
+ const SemanticValidator = require('./validators/SemanticValidator');
4
+ const ReferenceValidator = require('./validators/ReferenceValidator');
5
+ const ProvenanceValidator = require('./validators/ProvenanceValidator');
6
+ const chalk = require('chalk');
7
+
8
+ /**
9
+ * ValidationOrchestrator - Coordinates all validators and generates comprehensive reports
10
+ *
11
+ * Runs all validators in sequence and aggregates results
12
+ */
13
+ class ValidationOrchestrator {
14
+ constructor() {
15
+ this.validators = {
16
+ structural: new StructuralValidator(),
17
+ integrity: new IntegrityValidator(),
18
+ semantic: new SemanticValidator(),
19
+ reference: new ReferenceValidator(),
20
+ provenance: new ProvenanceValidator()
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Validate a single component with all validators
26
+ * @param {object} component - Component to validate
27
+ * @param {object} options - Validation options
28
+ * @param {Array<string>} options.validators - List of validators to run (default: all)
29
+ * @param {boolean} options.strict - Enable strict mode
30
+ * @param {boolean} options.updateRegistry - Update hash registry
31
+ * @returns {Promise<object>} Comprehensive validation results
32
+ */
33
+ async validateComponent(component, options = {}) {
34
+ const {
35
+ validators = ['structural', 'integrity', 'semantic', 'reference', 'provenance'],
36
+ strict = false,
37
+ updateRegistry = false
38
+ } = options;
39
+
40
+ const results = {
41
+ component: {
42
+ path: component.path,
43
+ type: component.type
44
+ },
45
+ timestamp: new Date().toISOString(),
46
+ overall: {
47
+ valid: true,
48
+ score: 0,
49
+ errorCount: 0,
50
+ warningCount: 0
51
+ },
52
+ validators: {}
53
+ };
54
+
55
+ // Run each validator
56
+ for (const validatorName of validators) {
57
+ if (!this.validators[validatorName]) {
58
+ console.warn(chalk.yellow(`⚠️ Unknown validator: ${validatorName}`));
59
+ continue;
60
+ }
61
+
62
+ try {
63
+ const validator = this.validators[validatorName];
64
+ let validatorOptions = {};
65
+
66
+ // Validator-specific options
67
+ if (validatorName === 'semantic') {
68
+ validatorOptions.strict = strict;
69
+ } else if (validatorName === 'integrity') {
70
+ validatorOptions.updateRegistry = updateRegistry;
71
+ }
72
+
73
+ const result = await validator.validate(component, validatorOptions);
74
+
75
+ results.validators[validatorName] = {
76
+ valid: result.valid,
77
+ score: result.score || 0,
78
+ errorCount: result.errorCount,
79
+ warningCount: result.warningCount,
80
+ errors: result.errors,
81
+ warnings: result.warnings,
82
+ info: result.info
83
+ };
84
+
85
+ // Add validator-specific metadata
86
+ if (result.hash) {
87
+ results.validators[validatorName].hash = result.hash;
88
+ }
89
+ if (result.metadata) {
90
+ results.validators[validatorName].metadata = result.metadata;
91
+ }
92
+
93
+ // Update overall results
94
+ if (!result.valid) {
95
+ results.overall.valid = false;
96
+ }
97
+ results.overall.errorCount += result.errorCount;
98
+ results.overall.warningCount += result.warningCount;
99
+
100
+ } catch (error) {
101
+ results.validators[validatorName] = {
102
+ valid: false,
103
+ error: error.message,
104
+ errorCount: 1,
105
+ warningCount: 0
106
+ };
107
+ results.overall.valid = false;
108
+ results.overall.errorCount++;
109
+ }
110
+ }
111
+
112
+ // Calculate overall score (average of all validator scores)
113
+ const scores = Object.values(results.validators)
114
+ .map(v => v.score || 0)
115
+ .filter(s => s > 0);
116
+
117
+ if (scores.length > 0) {
118
+ results.overall.score = Math.round(
119
+ scores.reduce((sum, score) => sum + score, 0) / scores.length
120
+ );
121
+ }
122
+
123
+ return results;
124
+ }
125
+
126
+ /**
127
+ * Validate multiple components
128
+ * @param {Array<object>} components - Components to validate
129
+ * @param {object} options - Validation options
130
+ * @returns {Promise<object>} Batch validation results
131
+ */
132
+ async validateComponents(components, options = {}) {
133
+ const results = {
134
+ summary: {
135
+ total: components.length,
136
+ passed: 0,
137
+ failed: 0,
138
+ warnings: 0
139
+ },
140
+ components: [],
141
+ timestamp: new Date().toISOString()
142
+ };
143
+
144
+ for (const component of components) {
145
+ const result = await this.validateComponent(component, options);
146
+
147
+ results.components.push(result);
148
+
149
+ if (result.overall.valid) {
150
+ results.summary.passed++;
151
+ } else {
152
+ results.summary.failed++;
153
+ }
154
+
155
+ results.summary.warnings += result.overall.warningCount;
156
+ }
157
+
158
+ return results;
159
+ }
160
+
161
+ /**
162
+ * Generate human-readable report
163
+ * @param {object} validationResults - Results from validateComponent or validateComponents
164
+ * @param {object} options - Report options
165
+ * @param {boolean} options.verbose - Include detailed information
166
+ * @param {boolean} options.colors - Use colored output (default: true)
167
+ * @returns {string} Formatted report
168
+ */
169
+ generateReport(validationResults, options = {}) {
170
+ const { verbose = false, colors = true } = options;
171
+ const lines = [];
172
+
173
+ // Helper functions for colored output
174
+ const success = (text) => colors ? chalk.green(text) : text;
175
+ const error = (text) => colors ? chalk.red(text) : text;
176
+ const warning = (text) => colors ? chalk.yellow(text) : text;
177
+ const info = (text) => colors ? chalk.blue(text) : text;
178
+ const dim = (text) => colors ? chalk.gray(text) : text;
179
+
180
+ // Check if this is a batch result or single component result
181
+ const isBatch = validationResults.summary && validationResults.components;
182
+
183
+ if (isBatch) {
184
+ // Batch report
185
+ lines.push('');
186
+ lines.push(info('🔒 Security Audit Report'));
187
+ lines.push(dim('━'.repeat(60)));
188
+ lines.push('');
189
+
190
+ lines.push(`📊 Summary:`);
191
+ lines.push(` Total components: ${validationResults.summary.total}`);
192
+ lines.push(` ${success('✅ Passed')}: ${validationResults.summary.passed}`);
193
+ lines.push(` ${error('❌ Failed')}: ${validationResults.summary.failed}`);
194
+ lines.push(` ${warning('⚠️ Warnings')}: ${validationResults.summary.warnings}`);
195
+ lines.push('');
196
+
197
+ // Component details
198
+ for (const component of validationResults.components) {
199
+ lines.push(this._formatComponentResult(component, verbose, { success, error, warning, info, dim }));
200
+ }
201
+ } else {
202
+ // Single component report
203
+ lines.push(this._formatComponentResult(validationResults, verbose, { success, error, warning, info, dim }));
204
+ }
205
+
206
+ lines.push(dim('━'.repeat(60)));
207
+ lines.push('');
208
+
209
+ return lines.join('\n');
210
+ }
211
+
212
+ /**
213
+ * Format a single component result
214
+ * @private
215
+ */
216
+ _formatComponentResult(componentResult, verbose, colors) {
217
+ const { success, error, warning, info, dim } = colors;
218
+ const lines = [];
219
+
220
+ const status = componentResult.overall.valid ? success('✅ PASS') : error('❌ FAIL');
221
+ const scoreBadge = this._getScoreBadge(componentResult.overall.score, colors);
222
+
223
+ lines.push(`${status} ${componentResult.component.path} ${scoreBadge}`);
224
+
225
+ // Validator breakdown
226
+ for (const [validatorName, result] of Object.entries(componentResult.validators)) {
227
+ const validatorStatus = result.valid ? success('✅') : error('❌');
228
+ const validatorScore = result.score ? dim(`(${result.score}/100)`) : '';
229
+
230
+ lines.push(` ├─ ${validatorStatus} ${validatorName}: ${result.errorCount === 0 ? 'PASS' : `${result.errorCount} errors`} ${validatorScore}`);
231
+
232
+ // Show errors
233
+ if (result.errors && result.errors.length > 0 && verbose) {
234
+ for (const err of result.errors.slice(0, 3)) {
235
+ lines.push(` │ ${error('ERROR')}: ${err.message} ${dim(`[${err.code}]`)}`);
236
+ }
237
+ if (result.errors.length > 3) {
238
+ lines.push(` │ ${dim(`... and ${result.errors.length - 3} more errors`)}`);
239
+ }
240
+ }
241
+
242
+ // Show warnings
243
+ if (result.warnings && result.warnings.length > 0 && verbose) {
244
+ for (const warn of result.warnings.slice(0, 2)) {
245
+ lines.push(` │ ${warning('WARNING')}: ${warn.message} ${dim(`[${warn.code}]`)}`);
246
+ }
247
+ if (result.warnings.length > 2) {
248
+ lines.push(` │ ${dim(`... and ${result.warnings.length - 2} more warnings`)}`);
249
+ }
250
+ }
251
+ }
252
+
253
+ lines.push('');
254
+ return lines.join('\n');
255
+ }
256
+
257
+ /**
258
+ * Get score badge with color
259
+ * @private
260
+ */
261
+ _getScoreBadge(score, colors) {
262
+ const { success, error, warning, dim } = colors;
263
+
264
+ if (score >= 90) return success(`[${score}/100]`);
265
+ if (score >= 70) return warning(`[${score}/100]`);
266
+ if (score >= 50) return error(`[${score}/100]`);
267
+ return dim(`[${score}/100]`);
268
+ }
269
+
270
+ /**
271
+ * Generate JSON report
272
+ * @param {object} validationResults - Results from validateComponent or validateComponents
273
+ * @returns {string} JSON formatted report
274
+ */
275
+ generateJsonReport(validationResults) {
276
+ return JSON.stringify(validationResults, null, 2);
277
+ }
278
+
279
+ /**
280
+ * Get all error codes from results
281
+ * @param {object} validationResults - Validation results
282
+ * @returns {Array<string>} Unique error codes
283
+ */
284
+ getErrorCodes(validationResults) {
285
+ const codes = new Set();
286
+
287
+ const processResult = (result) => {
288
+ for (const validator of Object.values(result.validators)) {
289
+ if (validator.errors) {
290
+ validator.errors.forEach(err => codes.add(err.code));
291
+ }
292
+ }
293
+ };
294
+
295
+ if (validationResults.components) {
296
+ validationResults.components.forEach(processResult);
297
+ } else {
298
+ processResult(validationResults);
299
+ }
300
+
301
+ return Array.from(codes).sort();
302
+ }
303
+ }
304
+
305
+ module.exports = ValidationOrchestrator;
@@ -0,0 +1,338 @@
1
+ const BaseValidator = require('../BaseValidator');
2
+ const crypto = require('crypto');
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * IntegrityValidator - Validates component integrity and versioning
8
+ *
9
+ * Checks:
10
+ * - SHA256 hash generation
11
+ * - Hash verification against stored hashes
12
+ * - Version tracking
13
+ * - Signature validation (future)
14
+ * - Tamper detection
15
+ */
16
+ class IntegrityValidator extends BaseValidator {
17
+ constructor() {
18
+ super();
19
+
20
+ // Path to store component hashes (relative to project)
21
+ this.HASH_REGISTRY_PATH = '.claude/security/component-hashes.json';
22
+ }
23
+
24
+ /**
25
+ * Validate component integrity
26
+ * @param {object} component - Component data
27
+ * @param {string} component.content - Raw markdown content
28
+ * @param {string} component.path - File path
29
+ * @param {string} component.type - Component type
30
+ * @param {string} component.version - Component version (optional)
31
+ * @param {object} options - Validation options
32
+ * @param {boolean} options.updateRegistry - Update hash registry after validation
33
+ * @param {string} options.expectedHash - Expected hash to verify against
34
+ * @returns {Promise<object>} Validation results with hash
35
+ */
36
+ async validate(component, options = {}) {
37
+ this.reset();
38
+
39
+ const { content, path: filePath, type, version } = component;
40
+ const { updateRegistry = false, expectedHash = null } = options;
41
+
42
+ if (!content) {
43
+ this.addError('INT_E001', 'Component content is empty or missing', { path: filePath });
44
+ return this.getResults();
45
+ }
46
+
47
+ // 1. Generate SHA256 hash
48
+ const hash = this.generateHash(content);
49
+ this.addInfo('INT_I001', `Generated SHA256 hash`, {
50
+ path: filePath,
51
+ hash: hash.substring(0, 16) + '...',
52
+ fullHash: hash
53
+ });
54
+
55
+ // 2. Verify against expected hash if provided
56
+ if (expectedHash) {
57
+ this.verifyHash(hash, expectedHash, filePath);
58
+ }
59
+
60
+ // 3. Check hash registry for changes
61
+ await this.checkHashRegistry(filePath, hash, type, version);
62
+
63
+ // 4. Validate version if provided
64
+ // Note: Version is optional for components - metadata is stored in marketplace.json
65
+ if (version) {
66
+ this.validateVersion(version, filePath);
67
+ } else {
68
+ this.addInfo('INT_I009', 'No version in component (metadata in marketplace.json)', {
69
+ path: filePath
70
+ });
71
+ }
72
+
73
+ // 5. Update registry if requested
74
+ if (updateRegistry) {
75
+ await this.updateHashRegistry(filePath, hash, type, version);
76
+ }
77
+
78
+ // Add hash to results for external use
79
+ const results = this.getResults();
80
+ results.hash = hash;
81
+ results.version = version;
82
+
83
+ return results;
84
+ }
85
+
86
+ /**
87
+ * Generate SHA256 hash of content
88
+ * @param {string} content - Content to hash
89
+ * @returns {string} SHA256 hash in hex format
90
+ */
91
+ generateHash(content) {
92
+ return crypto
93
+ .createHash('sha256')
94
+ .update(content, 'utf8')
95
+ .digest('hex');
96
+ }
97
+
98
+ /**
99
+ * Verify hash matches expected hash
100
+ * @param {string} actualHash - Generated hash
101
+ * @param {string} expectedHash - Expected hash
102
+ * @param {string} filePath - File path for error reporting
103
+ */
104
+ verifyHash(actualHash, expectedHash, filePath) {
105
+ if (actualHash !== expectedHash) {
106
+ this.addError(
107
+ 'INT_E002',
108
+ 'Hash mismatch: Component content has been modified',
109
+ {
110
+ path: filePath,
111
+ expected: expectedHash.substring(0, 16) + '...',
112
+ actual: actualHash.substring(0, 16) + '...',
113
+ fullExpected: expectedHash,
114
+ fullActual: actualHash
115
+ }
116
+ );
117
+ } else {
118
+ this.addInfo('INT_I002', 'Hash verification passed', { path: filePath });
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Check component hash against registry
124
+ * @param {string} filePath - File path
125
+ * @param {string} currentHash - Current hash
126
+ * @param {string} type - Component type
127
+ * @param {string} version - Component version
128
+ */
129
+ async checkHashRegistry(filePath, currentHash, type, version) {
130
+ try {
131
+ const registry = await this.loadHashRegistry();
132
+ const normalizedPath = this.normalizePath(filePath);
133
+
134
+ if (registry[normalizedPath]) {
135
+ const stored = registry[normalizedPath];
136
+
137
+ // Check if hash has changed
138
+ if (stored.hash !== currentHash) {
139
+ this.addWarning(
140
+ 'INT_W001',
141
+ 'Component hash has changed since last validation',
142
+ {
143
+ path: filePath,
144
+ previousHash: stored.hash.substring(0, 16) + '...',
145
+ currentHash: currentHash.substring(0, 16) + '...',
146
+ lastValidated: stored.timestamp
147
+ }
148
+ );
149
+ } else {
150
+ this.addInfo('INT_I003', 'Hash matches registry', {
151
+ path: filePath,
152
+ lastValidated: stored.timestamp
153
+ });
154
+ }
155
+
156
+ // Check version changes
157
+ if (version && stored.version && stored.version !== version) {
158
+ this.addInfo('INT_I004', 'Version updated', {
159
+ path: filePath,
160
+ previousVersion: stored.version,
161
+ currentVersion: version
162
+ });
163
+ }
164
+ } else {
165
+ this.addInfo('INT_I005', 'Component not in registry (new component)', {
166
+ path: filePath
167
+ });
168
+ }
169
+ } catch (error) {
170
+ // Registry doesn't exist or couldn't be read - this is OK for new setups
171
+ this.addInfo('INT_I006', 'Hash registry not found (first run)', {
172
+ path: filePath
173
+ });
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Validate version format
179
+ * @param {string} version - Version string
180
+ * @param {string} filePath - File path for error reporting
181
+ */
182
+ validateVersion(version, filePath) {
183
+ // Support semantic versioning (X.Y.Z) and simple versions
184
+ const semverPattern = /^\d+\.\d+\.\d+$/;
185
+ const simplePattern = /^\d+(\.\d+)?$/;
186
+
187
+ if (!semverPattern.test(version) && !simplePattern.test(version)) {
188
+ this.addWarning(
189
+ 'INT_W003',
190
+ `Version format "${version}" doesn't follow semantic versioning (X.Y.Z)`,
191
+ {
192
+ path: filePath,
193
+ version,
194
+ recommendation: 'Use semantic versioning (e.g., 1.0.0)'
195
+ }
196
+ );
197
+ } else {
198
+ this.addInfo('INT_I007', `Valid version: ${version}`, { path: filePath, version });
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Update hash registry with new hash
204
+ * @param {string} filePath - File path
205
+ * @param {string} hash - Current hash
206
+ * @param {string} type - Component type
207
+ * @param {string} version - Component version
208
+ */
209
+ async updateHashRegistry(filePath, hash, type, version) {
210
+ try {
211
+ const registry = await this.loadHashRegistry();
212
+ const normalizedPath = this.normalizePath(filePath);
213
+
214
+ registry[normalizedPath] = {
215
+ hash,
216
+ type,
217
+ version: version || 'unversioned',
218
+ timestamp: new Date().toISOString(),
219
+ path: filePath
220
+ };
221
+
222
+ await this.saveHashRegistry(registry);
223
+
224
+ this.addInfo('INT_I008', 'Hash registry updated', {
225
+ path: filePath,
226
+ hash: hash.substring(0, 16) + '...'
227
+ });
228
+ } catch (error) {
229
+ this.addWarning(
230
+ 'INT_W004',
231
+ `Failed to update hash registry: ${error.message}`,
232
+ { path: filePath, error: error.message }
233
+ );
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Load hash registry from file
239
+ * @returns {Promise<object>} Registry object
240
+ */
241
+ async loadHashRegistry() {
242
+ const registryPath = path.join(process.cwd(), this.HASH_REGISTRY_PATH);
243
+
244
+ if (await fs.pathExists(registryPath)) {
245
+ return await fs.readJson(registryPath);
246
+ }
247
+
248
+ return {};
249
+ }
250
+
251
+ /**
252
+ * Save hash registry to file
253
+ * @param {object} registry - Registry object to save
254
+ */
255
+ async saveHashRegistry(registry) {
256
+ const registryPath = path.join(process.cwd(), this.HASH_REGISTRY_PATH);
257
+
258
+ // Ensure directory exists
259
+ await fs.ensureDir(path.dirname(registryPath));
260
+
261
+ // Save with pretty formatting
262
+ await fs.writeJson(registryPath, registry, { spaces: 2 });
263
+ }
264
+
265
+ /**
266
+ * Normalize file path for consistent registry keys
267
+ * @param {string} filePath - File path to normalize
268
+ * @returns {string} Normalized path
269
+ */
270
+ normalizePath(filePath) {
271
+ // Convert to relative path from project root
272
+ const cwd = process.cwd();
273
+ if (filePath.startsWith(cwd)) {
274
+ return path.relative(cwd, filePath);
275
+ }
276
+ return filePath;
277
+ }
278
+
279
+ /**
280
+ * Generate integrity report for a component
281
+ * @param {object} component - Component data
282
+ * @returns {Promise<object>} Integrity report
283
+ */
284
+ async generateIntegrityReport(component) {
285
+ const result = await this.validate(component);
286
+
287
+ return {
288
+ valid: result.valid,
289
+ hash: result.hash,
290
+ version: result.version,
291
+ timestamp: new Date().toISOString(),
292
+ issues: {
293
+ errors: result.errors,
294
+ warnings: result.warnings
295
+ }
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Batch validate multiple components
301
+ * @param {Array<object>} components - Array of components to validate
302
+ * @param {object} options - Validation options
303
+ * @returns {Promise<object>} Batch validation results
304
+ */
305
+ async batchValidate(components, options = {}) {
306
+ const results = {
307
+ total: components.length,
308
+ passed: 0,
309
+ failed: 0,
310
+ warnings: 0,
311
+ components: []
312
+ };
313
+
314
+ for (const component of components) {
315
+ const result = await this.validate(component, options);
316
+
317
+ results.components.push({
318
+ path: component.path,
319
+ valid: result.valid,
320
+ hash: result.hash,
321
+ errors: result.errorCount,
322
+ warnings: result.warningCount
323
+ });
324
+
325
+ if (result.valid) {
326
+ results.passed++;
327
+ } else {
328
+ results.failed++;
329
+ }
330
+
331
+ results.warnings += result.warningCount;
332
+ }
333
+
334
+ return results;
335
+ }
336
+ }
337
+
338
+ module.exports = IntegrityValidator;