claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
339 lines (298 loc) • 9.78 kB
JavaScript
const BaseValidator = require('../BaseValidator');
const crypto = require('crypto');
const fs = require('fs-extra');
const path = require('path');
/**
* IntegrityValidator - Validates component integrity and versioning
*
* Checks:
* - SHA256 hash generation
* - Hash verification against stored hashes
* - Version tracking
* - Signature validation (future)
* - Tamper detection
*/
class IntegrityValidator extends BaseValidator {
constructor() {
super();
// Path to store component hashes (relative to project)
this.HASH_REGISTRY_PATH = '.claude/security/component-hashes.json';
}
/**
* Validate component integrity
* @param {object} component - Component data
* @param {string} component.content - Raw markdown content
* @param {string} component.path - File path
* @param {string} component.type - Component type
* @param {string} component.version - Component version (optional)
* @param {object} options - Validation options
* @param {boolean} options.updateRegistry - Update hash registry after validation
* @param {string} options.expectedHash - Expected hash to verify against
* @returns {Promise<object>} Validation results with hash
*/
async validate(component, options = {}) {
this.reset();
const { content, path: filePath, type, version } = component;
const { updateRegistry = false, expectedHash = null } = options;
if (!content) {
this.addError('INT_E001', 'Component content is empty or missing', { path: filePath });
return this.getResults();
}
// 1. Generate SHA256 hash
const hash = this.generateHash(content);
this.addInfo('INT_I001', `Generated SHA256 hash`, {
path: filePath,
hash: hash.substring(0, 16) + '...',
fullHash: hash
});
// 2. Verify against expected hash if provided
if (expectedHash) {
this.verifyHash(hash, expectedHash, filePath);
}
// 3. Check hash registry for changes
await this.checkHashRegistry(filePath, hash, type, version);
// 4. Validate version if provided
// Note: Version is optional for components - metadata is stored in marketplace.json
if (version) {
this.validateVersion(version, filePath);
} else {
this.addInfo('INT_I009', 'No version in component (metadata in marketplace.json)', {
path: filePath
});
}
// 5. Update registry if requested
if (updateRegistry) {
await this.updateHashRegistry(filePath, hash, type, version);
}
// Add hash to results for external use
const results = this.getResults();
results.hash = hash;
results.version = version;
return results;
}
/**
* Generate SHA256 hash of content
* @param {string} content - Content to hash
* @returns {string} SHA256 hash in hex format
*/
generateHash(content) {
return crypto
.createHash('sha256')
.update(content, 'utf8')
.digest('hex');
}
/**
* Verify hash matches expected hash
* @param {string} actualHash - Generated hash
* @param {string} expectedHash - Expected hash
* @param {string} filePath - File path for error reporting
*/
verifyHash(actualHash, expectedHash, filePath) {
if (actualHash !== expectedHash) {
this.addError(
'INT_E002',
'Hash mismatch: Component content has been modified',
{
path: filePath,
expected: expectedHash.substring(0, 16) + '...',
actual: actualHash.substring(0, 16) + '...',
fullExpected: expectedHash,
fullActual: actualHash
}
);
} else {
this.addInfo('INT_I002', 'Hash verification passed', { path: filePath });
}
}
/**
* Check component hash against registry
* @param {string} filePath - File path
* @param {string} currentHash - Current hash
* @param {string} type - Component type
* @param {string} version - Component version
*/
async checkHashRegistry(filePath, currentHash, type, version) {
try {
const registry = await this.loadHashRegistry();
const normalizedPath = this.normalizePath(filePath);
if (registry[normalizedPath]) {
const stored = registry[normalizedPath];
// Check if hash has changed
if (stored.hash !== currentHash) {
this.addWarning(
'INT_W001',
'Component hash has changed since last validation',
{
path: filePath,
previousHash: stored.hash.substring(0, 16) + '...',
currentHash: currentHash.substring(0, 16) + '...',
lastValidated: stored.timestamp
}
);
} else {
this.addInfo('INT_I003', 'Hash matches registry', {
path: filePath,
lastValidated: stored.timestamp
});
}
// Check version changes
if (version && stored.version && stored.version !== version) {
this.addInfo('INT_I004', 'Version updated', {
path: filePath,
previousVersion: stored.version,
currentVersion: version
});
}
} else {
this.addInfo('INT_I005', 'Component not in registry (new component)', {
path: filePath
});
}
} catch (error) {
// Registry doesn't exist or couldn't be read - this is OK for new setups
this.addInfo('INT_I006', 'Hash registry not found (first run)', {
path: filePath
});
}
}
/**
* Validate version format
* @param {string} version - Version string
* @param {string} filePath - File path for error reporting
*/
validateVersion(version, filePath) {
// Support semantic versioning (X.Y.Z) and simple versions
const semverPattern = /^\d+\.\d+\.\d+$/;
const simplePattern = /^\d+(\.\d+)?$/;
if (!semverPattern.test(version) && !simplePattern.test(version)) {
this.addWarning(
'INT_W003',
`Version format "${version}" doesn't follow semantic versioning (X.Y.Z)`,
{
path: filePath,
version,
recommendation: 'Use semantic versioning (e.g., 1.0.0)'
}
);
} else {
this.addInfo('INT_I007', `Valid version: ${version}`, { path: filePath, version });
}
}
/**
* Update hash registry with new hash
* @param {string} filePath - File path
* @param {string} hash - Current hash
* @param {string} type - Component type
* @param {string} version - Component version
*/
async updateHashRegistry(filePath, hash, type, version) {
try {
const registry = await this.loadHashRegistry();
const normalizedPath = this.normalizePath(filePath);
registry[normalizedPath] = {
hash,
type,
version: version || 'unversioned',
timestamp: new Date().toISOString(),
path: filePath
};
await this.saveHashRegistry(registry);
this.addInfo('INT_I008', 'Hash registry updated', {
path: filePath,
hash: hash.substring(0, 16) + '...'
});
} catch (error) {
this.addWarning(
'INT_W004',
`Failed to update hash registry: ${error.message}`,
{ path: filePath, error: error.message }
);
}
}
/**
* Load hash registry from file
* @returns {Promise<object>} Registry object
*/
async loadHashRegistry() {
const registryPath = path.join(process.cwd(), this.HASH_REGISTRY_PATH);
if (await fs.pathExists(registryPath)) {
return await fs.readJson(registryPath);
}
return {};
}
/**
* Save hash registry to file
* @param {object} registry - Registry object to save
*/
async saveHashRegistry(registry) {
const registryPath = path.join(process.cwd(), this.HASH_REGISTRY_PATH);
// Ensure directory exists
await fs.ensureDir(path.dirname(registryPath));
// Save with pretty formatting
await fs.writeJson(registryPath, registry, { spaces: 2 });
}
/**
* Normalize file path for consistent registry keys
* @param {string} filePath - File path to normalize
* @returns {string} Normalized path
*/
normalizePath(filePath) {
// Convert to relative path from project root
const cwd = process.cwd();
if (filePath.startsWith(cwd)) {
return path.relative(cwd, filePath);
}
return filePath;
}
/**
* Generate integrity report for a component
* @param {object} component - Component data
* @returns {Promise<object>} Integrity report
*/
async generateIntegrityReport(component) {
const result = await this.validate(component);
return {
valid: result.valid,
hash: result.hash,
version: result.version,
timestamp: new Date().toISOString(),
issues: {
errors: result.errors,
warnings: result.warnings
}
};
}
/**
* Batch validate multiple components
* @param {Array<object>} components - Array of components to validate
* @param {object} options - Validation options
* @returns {Promise<object>} Batch validation results
*/
async batchValidate(components, options = {}) {
const results = {
total: components.length,
passed: 0,
failed: 0,
warnings: 0,
components: []
};
for (const component of components) {
const result = await this.validate(component, options);
results.components.push({
path: component.path,
valid: result.valid,
hash: result.hash,
errors: result.errorCount,
warnings: result.warningCount
});
if (result.valid) {
results.passed++;
} else {
results.failed++;
}
results.warnings += result.warningCount;
}
return results;
}
}
module.exports = IntegrityValidator;