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,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;
|