@paulduvall/claude-dev-toolkit 0.0.1-alpha.1
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 +254 -0
- package/bin/claude-commands +132 -0
- package/lib/claude-code-compatibility.js +545 -0
- package/lib/command-selector.js +245 -0
- package/lib/config.js +182 -0
- package/lib/context-utils.js +80 -0
- package/lib/dependency-validator.js +354 -0
- package/lib/error-factory.js +394 -0
- package/lib/error-handler-utils.js +432 -0
- package/lib/error-recovery-system.js +563 -0
- package/lib/failure-recovery-installer.js +370 -0
- package/lib/hook-installer-core.js +330 -0
- package/lib/hook-installer.js +187 -0
- package/lib/hook-metadata-service.js +352 -0
- package/lib/hook-validator.js +358 -0
- package/lib/installation-configuration.js +380 -0
- package/lib/installation-instruction-generator.js +564 -0
- package/lib/installer.js +68 -0
- package/lib/package-manager-service.js +270 -0
- package/lib/permission-error-handler.js +543 -0
- package/lib/platform-utils.js +491 -0
- package/lib/setup-wizard-ui.js +245 -0
- package/lib/setup-wizard.js +355 -0
- package/lib/system-requirements-checker.js +558 -0
- package/lib/utils.js +15 -0
- package/lib/validation-utils.js +320 -0
- package/lib/version-validator-service.js +326 -0
- package/package.json +73 -0
- package/scripts/postinstall.js +182 -0
- package/scripts/validate.js +94 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Security Hook Installer for Claude Dev Toolkit
|
|
2
|
+
// Implements REQ-018: Security Hook Installation
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Import modular components for better separation of concerns
|
|
7
|
+
const HookInstaller = require('./hook-installer-core');
|
|
8
|
+
const HookValidator = require('./hook-validator');
|
|
9
|
+
const HookMetadataService = require('./hook-metadata-service');
|
|
10
|
+
|
|
11
|
+
// Create singleton instances
|
|
12
|
+
const hookInstaller = new HookInstaller();
|
|
13
|
+
const hookValidator = new HookValidator();
|
|
14
|
+
const hookMetadataService = new HookMetadataService();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Install security hooks to the specified directory
|
|
18
|
+
* Implements REQ-018: Security Hook Installation
|
|
19
|
+
*
|
|
20
|
+
* @param {string} targetHooksDir - Directory to install hooks to
|
|
21
|
+
* @param {Array|string} hookNames - Array of hook names or single hook name to install
|
|
22
|
+
* @param {Object} options - Installation options
|
|
23
|
+
* @param {boolean} options.force - Force overwrite existing hooks
|
|
24
|
+
* @param {boolean} options.validate - Validate hooks before installation
|
|
25
|
+
* @param {boolean} options.backup - Create backup of existing hooks
|
|
26
|
+
* @returns {Object} - Installation result with details
|
|
27
|
+
*/
|
|
28
|
+
function installSecurityHooks(targetHooksDir, hookNames, options = {}) {
|
|
29
|
+
return hookInstaller.installSecurityHooks(targetHooksDir, hookNames, options);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Legacy functions maintained for backward compatibility
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the source hooks directory path
|
|
36
|
+
* @returns {string} - Path to source hooks directory
|
|
37
|
+
*/
|
|
38
|
+
function getSourceHooksDirectory() {
|
|
39
|
+
return hookInstaller.getSourceHooksDirectory();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get available security hooks with caching for performance
|
|
44
|
+
* @param {boolean} forceRefresh - Force refresh of the cache
|
|
45
|
+
* @returns {Array} - Array of available hook information
|
|
46
|
+
*/
|
|
47
|
+
function getAvailableHooks(forceRefresh = false) {
|
|
48
|
+
return hookMetadataService.getAvailableHooks(forceRefresh);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Legacy functions - delegated to metadata service for backward compatibility
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get detailed metadata from hook file
|
|
55
|
+
* @param {string} hookPath - Path to hook file
|
|
56
|
+
* @returns {Object} - Hook metadata
|
|
57
|
+
*/
|
|
58
|
+
function getHookMetadata(hookPath) {
|
|
59
|
+
const hooks = hookMetadataService.getAvailableHooks();
|
|
60
|
+
const hookName = path.basename(hookPath, '.sh');
|
|
61
|
+
const hook = hooks.find(h => h.name === hookName);
|
|
62
|
+
return hook ? hook.metadata : {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get file size in bytes
|
|
67
|
+
* @param {string} filePath - Path to file
|
|
68
|
+
* @returns {number} - File size in bytes
|
|
69
|
+
*/
|
|
70
|
+
function getFileSize(filePath) {
|
|
71
|
+
const hooks = hookMetadataService.getAvailableHooks();
|
|
72
|
+
const hookName = path.basename(filePath, '.sh');
|
|
73
|
+
const hook = hooks.find(h => h.name === hookName);
|
|
74
|
+
return hook ? hook.size : 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get description from hook file
|
|
79
|
+
* @param {string} hookPath - Path to hook file
|
|
80
|
+
* @returns {string} - Hook description
|
|
81
|
+
*/
|
|
82
|
+
function getHookDescription(hookPath) {
|
|
83
|
+
const hooks = hookMetadataService.getAvailableHooks();
|
|
84
|
+
const hookName = path.basename(hookPath, '.sh');
|
|
85
|
+
const hook = hooks.find(h => h.name === hookName);
|
|
86
|
+
return hook ? hook.description : 'Security hook';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate a hook file
|
|
91
|
+
* @param {string} hookPath - Path to hook file
|
|
92
|
+
* @returns {boolean} - True if valid, false otherwise
|
|
93
|
+
*/
|
|
94
|
+
function validateHook(hookPath) {
|
|
95
|
+
const result = hookValidator.validateHook(hookPath);
|
|
96
|
+
return result.valid;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get installation log
|
|
101
|
+
* @param {boolean} clear - Whether to clear the log after retrieving
|
|
102
|
+
* @returns {Array} - Installation log entries
|
|
103
|
+
*/
|
|
104
|
+
function getInstallationLog(clear = false) {
|
|
105
|
+
return hookInstaller.getInstallationLog(clear);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Remove installed security hooks
|
|
110
|
+
* @param {string} targetHooksDir - Directory containing installed hooks
|
|
111
|
+
* @param {Array|string} hookNames - Array of hook names or single hook name to remove
|
|
112
|
+
* @returns {Object} - Removal result with details
|
|
113
|
+
*/
|
|
114
|
+
function removeSecurityHooks(targetHooksDir, hookNames) {
|
|
115
|
+
return hookInstaller.removeSecurityHooks(targetHooksDir, hookNames);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get hook installation summary
|
|
120
|
+
* @returns {Object} - Summary of hook installations and system status
|
|
121
|
+
*/
|
|
122
|
+
function getHookInstallationSummary() {
|
|
123
|
+
const installerSummary = hookInstaller.getHookInstallationSummary();
|
|
124
|
+
const metadataStats = hookMetadataService.getHookStats();
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
...installerSummary,
|
|
128
|
+
availableHooks: metadataStats.total,
|
|
129
|
+
validHooks: metadataStats.valid
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Clear hook metadata cache (useful for testing or after hook updates)
|
|
135
|
+
*/
|
|
136
|
+
function clearHookCache() {
|
|
137
|
+
hookMetadataService.clearCache();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Backward-compatible wrapper for installSecurityHooks
|
|
142
|
+
* Returns boolean for simple cases, detailed object for complex cases
|
|
143
|
+
*/
|
|
144
|
+
function installSecurityHooksCompat(targetHooksDir, hookNames, options = {}) {
|
|
145
|
+
const result = installSecurityHooks(targetHooksDir, hookNames, options);
|
|
146
|
+
|
|
147
|
+
// For backward compatibility, return boolean if no options specified
|
|
148
|
+
if (!options || Object.keys(options).length === 0) {
|
|
149
|
+
return result.success;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Return full result object for advanced usage
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
// Core functionality (REQ-018 implementation)
|
|
158
|
+
installSecurityHooks: installSecurityHooksCompat,
|
|
159
|
+
removeSecurityHooks,
|
|
160
|
+
getAvailableHooks,
|
|
161
|
+
validateHook,
|
|
162
|
+
|
|
163
|
+
// Logging and monitoring
|
|
164
|
+
getInstallationLog,
|
|
165
|
+
getHookInstallationSummary,
|
|
166
|
+
|
|
167
|
+
// Utility functions
|
|
168
|
+
clearHookCache,
|
|
169
|
+
|
|
170
|
+
// Internal functions exposed for testing (legacy support)
|
|
171
|
+
getSourceHooksDirectory,
|
|
172
|
+
getHookMetadata,
|
|
173
|
+
getFileSize,
|
|
174
|
+
|
|
175
|
+
// Advanced API (returns detailed objects)
|
|
176
|
+
installSecurityHooksDetailed: installSecurityHooks,
|
|
177
|
+
|
|
178
|
+
// Access to modular components for advanced usage
|
|
179
|
+
HookInstaller,
|
|
180
|
+
HookValidator,
|
|
181
|
+
HookMetadataService,
|
|
182
|
+
|
|
183
|
+
// Component instances
|
|
184
|
+
installer: hookInstaller,
|
|
185
|
+
validator: hookValidator,
|
|
186
|
+
metadataService: hookMetadataService
|
|
187
|
+
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Manages hook metadata and discovery
|
|
6
|
+
* Extracted from hook-installer.js for better separation of concerns
|
|
7
|
+
*/
|
|
8
|
+
class HookMetadataService {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.metadataCache = null;
|
|
11
|
+
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
|
|
12
|
+
this.lastCacheUpdate = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get available security hooks with caching for performance
|
|
17
|
+
* @param {boolean} forceRefresh - Force refresh of the cache
|
|
18
|
+
* @returns {Array} Array of available hook information
|
|
19
|
+
*/
|
|
20
|
+
getAvailableHooks(forceRefresh = false) {
|
|
21
|
+
try {
|
|
22
|
+
// Return cached data if available and not forcing refresh
|
|
23
|
+
if (this._isCacheValid() && !forceRefresh) {
|
|
24
|
+
return this.metadataCache;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sourceHooksDir = this._getSourceHooksDirectory();
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(sourceHooksDir)) {
|
|
30
|
+
this.metadataCache = [];
|
|
31
|
+
this.lastCacheUpdate = Date.now();
|
|
32
|
+
return this.metadataCache;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hookFiles = fs.readdirSync(sourceHooksDir).filter(f => f.endsWith('.sh'));
|
|
36
|
+
|
|
37
|
+
this.metadataCache = hookFiles.map(file => {
|
|
38
|
+
const name = path.basename(file, '.sh');
|
|
39
|
+
const hookPath = path.join(sourceHooksDir, file);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
filename: file,
|
|
44
|
+
path: hookPath,
|
|
45
|
+
description: this._getHookDescription(hookPath),
|
|
46
|
+
metadata: this._getHookMetadata(hookPath),
|
|
47
|
+
valid: this._isHookValid(hookPath),
|
|
48
|
+
size: this._getFileSize(hookPath),
|
|
49
|
+
lastModified: this._getLastModified(hookPath),
|
|
50
|
+
checksum: this._calculateChecksum(hookPath)
|
|
51
|
+
};
|
|
52
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
53
|
+
|
|
54
|
+
this.lastCacheUpdate = Date.now();
|
|
55
|
+
return this.metadataCache;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
this.metadataCache = [];
|
|
58
|
+
this.lastCacheUpdate = Date.now();
|
|
59
|
+
return this.metadataCache;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get metadata for a specific hook
|
|
65
|
+
* @param {string} hookName - Name of the hook
|
|
66
|
+
* @returns {Object|null} Hook metadata or null if not found
|
|
67
|
+
*/
|
|
68
|
+
getHookMetadata(hookName) {
|
|
69
|
+
const hooks = this.getAvailableHooks();
|
|
70
|
+
return hooks.find(hook => hook.name === hookName) || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Search hooks by criteria
|
|
75
|
+
* @param {Object} criteria - Search criteria
|
|
76
|
+
* @param {string} criteria.name - Hook name pattern
|
|
77
|
+
* @param {string} criteria.category - Hook category
|
|
78
|
+
* @param {boolean} criteria.valid - Only valid hooks
|
|
79
|
+
* @param {Array<string>} criteria.tools - Required tools
|
|
80
|
+
* @returns {Array} Matching hooks
|
|
81
|
+
*/
|
|
82
|
+
searchHooks(criteria = {}) {
|
|
83
|
+
let hooks = this.getAvailableHooks();
|
|
84
|
+
|
|
85
|
+
if (criteria.name) {
|
|
86
|
+
const namePattern = new RegExp(criteria.name, 'i');
|
|
87
|
+
hooks = hooks.filter(hook => namePattern.test(hook.name));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (criteria.category) {
|
|
91
|
+
hooks = hooks.filter(hook =>
|
|
92
|
+
hook.metadata.category === criteria.category
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (criteria.valid !== undefined) {
|
|
97
|
+
hooks = hooks.filter(hook => hook.valid === criteria.valid);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (criteria.tools && criteria.tools.length > 0) {
|
|
101
|
+
hooks = hooks.filter(hook =>
|
|
102
|
+
criteria.tools.some(tool => hook.metadata.tools.includes(tool))
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return hooks;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get hook categories with counts
|
|
111
|
+
* @returns {Object} Categories mapped to hook counts
|
|
112
|
+
*/
|
|
113
|
+
getHookCategories() {
|
|
114
|
+
const hooks = this.getAvailableHooks();
|
|
115
|
+
const categories = {};
|
|
116
|
+
|
|
117
|
+
hooks.forEach(hook => {
|
|
118
|
+
const category = hook.metadata.category || 'uncategorized';
|
|
119
|
+
categories[category] = (categories[category] || 0) + 1;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return categories;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get hooks statistics
|
|
127
|
+
* @returns {Object} Statistics about available hooks
|
|
128
|
+
*/
|
|
129
|
+
getHookStats() {
|
|
130
|
+
const hooks = this.getAvailableHooks();
|
|
131
|
+
const validHooks = hooks.filter(hook => hook.valid);
|
|
132
|
+
const categories = this.getHookCategories();
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
total: hooks.length,
|
|
136
|
+
valid: validHooks.length,
|
|
137
|
+
invalid: hooks.length - validHooks.length,
|
|
138
|
+
categories: Object.keys(categories).length,
|
|
139
|
+
categoryBreakdown: categories,
|
|
140
|
+
averageSize: hooks.length > 0 ?
|
|
141
|
+
Math.round(hooks.reduce((sum, hook) => sum + hook.size, 0) / hooks.length) : 0,
|
|
142
|
+
lastUpdated: this.lastCacheUpdate,
|
|
143
|
+
cacheStatus: this._isCacheValid() ? 'valid' : 'expired'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validate hook file content
|
|
149
|
+
* @param {string} hookPath - Path to hook file
|
|
150
|
+
* @returns {boolean} True if valid, false otherwise
|
|
151
|
+
*/
|
|
152
|
+
validateHook(hookPath) {
|
|
153
|
+
return this._isHookValid(hookPath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Clear metadata cache
|
|
158
|
+
*/
|
|
159
|
+
clearCache() {
|
|
160
|
+
this.metadataCache = null;
|
|
161
|
+
this.lastCacheUpdate = null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Export hook metadata to JSON
|
|
166
|
+
* @returns {string} JSON representation of all hook metadata
|
|
167
|
+
*/
|
|
168
|
+
exportMetadata() {
|
|
169
|
+
const metadata = {
|
|
170
|
+
generated: new Date().toISOString(),
|
|
171
|
+
stats: this.getHookStats(),
|
|
172
|
+
hooks: this.getAvailableHooks()
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return JSON.stringify(metadata, null, 2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if cache is valid (private method)
|
|
180
|
+
* @returns {boolean} True if cache is valid
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
_isCacheValid() {
|
|
184
|
+
if (!this.metadataCache || !this.lastCacheUpdate) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (Date.now() - this.lastCacheUpdate) < this.cacheTimeout;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get source hooks directory (private method)
|
|
193
|
+
* @returns {string} Path to source hooks directory
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
_getSourceHooksDirectory() {
|
|
197
|
+
return path.join(__dirname, '../hooks');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get detailed metadata from hook file (private method)
|
|
202
|
+
* @param {string} hookPath - Path to hook file
|
|
203
|
+
* @returns {Object} Hook metadata
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
_getHookMetadata(hookPath) {
|
|
207
|
+
const metadata = {
|
|
208
|
+
trigger: 'PreToolUse',
|
|
209
|
+
blocking: true,
|
|
210
|
+
tools: [],
|
|
211
|
+
author: 'Claude Dev Toolkit',
|
|
212
|
+
version: '1.0.0',
|
|
213
|
+
category: 'security'
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const content = fs.readFileSync(hookPath, 'utf8');
|
|
218
|
+
const lines = content.split('\n').slice(0, 20); // Check first 20 lines
|
|
219
|
+
|
|
220
|
+
for (const line of lines) {
|
|
221
|
+
const cleanLine = line.replace(/^#\s*/, '').trim();
|
|
222
|
+
|
|
223
|
+
if (cleanLine.startsWith('Trigger:')) {
|
|
224
|
+
metadata.trigger = cleanLine.replace('Trigger:', '').trim();
|
|
225
|
+
} else if (cleanLine.startsWith('Blocking:')) {
|
|
226
|
+
metadata.blocking = cleanLine.replace('Blocking:', '').trim().toLowerCase() === 'yes';
|
|
227
|
+
} else if (cleanLine.startsWith('Tools:')) {
|
|
228
|
+
const toolsStr = cleanLine.replace('Tools:', '').trim();
|
|
229
|
+
metadata.tools = toolsStr.split(',').map(t => t.trim()).filter(Boolean);
|
|
230
|
+
} else if (cleanLine.startsWith('Author:')) {
|
|
231
|
+
metadata.author = cleanLine.replace('Author:', '').trim();
|
|
232
|
+
} else if (cleanLine.startsWith('Version:')) {
|
|
233
|
+
metadata.version = cleanLine.replace('Version:', '').trim();
|
|
234
|
+
} else if (cleanLine.startsWith('Category:')) {
|
|
235
|
+
metadata.category = cleanLine.replace('Category:', '').trim();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return metadata;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return metadata;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get description from hook file (private method)
|
|
247
|
+
* @param {string} hookPath - Path to hook file
|
|
248
|
+
* @returns {string} Hook description
|
|
249
|
+
* @private
|
|
250
|
+
*/
|
|
251
|
+
_getHookDescription(hookPath) {
|
|
252
|
+
try {
|
|
253
|
+
const content = fs.readFileSync(hookPath, 'utf8');
|
|
254
|
+
const lines = content.split('\n');
|
|
255
|
+
|
|
256
|
+
// Look for description comment in first few lines
|
|
257
|
+
for (const line of lines.slice(0, 10)) {
|
|
258
|
+
if (line.includes('Description:') || line.includes('Purpose:')) {
|
|
259
|
+
return line.replace(/^#\s*/, '').replace(/^Description:\s*/i, '').replace(/^Purpose:\s*/i, '');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Default description based on hook name
|
|
264
|
+
const name = path.basename(hookPath, '.sh');
|
|
265
|
+
return `${name.replace(/-/g, ' ')} security hook`;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return 'Security hook';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get file size in bytes (private method)
|
|
273
|
+
* @param {string} filePath - Path to file
|
|
274
|
+
* @returns {number} File size in bytes
|
|
275
|
+
* @private
|
|
276
|
+
*/
|
|
277
|
+
_getFileSize(filePath) {
|
|
278
|
+
try {
|
|
279
|
+
const stats = fs.statSync(filePath);
|
|
280
|
+
return stats.size;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get file last modified time (private method)
|
|
288
|
+
* @param {string} filePath - Path to file
|
|
289
|
+
* @returns {Date|null} Last modified time or null
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
_getLastModified(filePath) {
|
|
293
|
+
try {
|
|
294
|
+
const stats = fs.statSync(filePath);
|
|
295
|
+
return stats.mtime;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Calculate file checksum (private method)
|
|
303
|
+
* @param {string} filePath - Path to file
|
|
304
|
+
* @returns {string} MD5 checksum or empty string
|
|
305
|
+
* @private
|
|
306
|
+
*/
|
|
307
|
+
_calculateChecksum(filePath) {
|
|
308
|
+
try {
|
|
309
|
+
const crypto = require('crypto');
|
|
310
|
+
const content = fs.readFileSync(filePath);
|
|
311
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return '';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Check if hook is valid (private method)
|
|
319
|
+
* @param {string} hookPath - Path to hook file
|
|
320
|
+
* @returns {boolean} True if valid, false otherwise
|
|
321
|
+
* @private
|
|
322
|
+
*/
|
|
323
|
+
_isHookValid(hookPath) {
|
|
324
|
+
try {
|
|
325
|
+
if (!fs.existsSync(hookPath)) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const content = fs.readFileSync(hookPath, 'utf8');
|
|
330
|
+
|
|
331
|
+
// Basic validation: should have shebang and be executable
|
|
332
|
+
const validShebangs = ['#!/bin/bash', '#!/bin/sh', '#!/usr/bin/env bash', '#!/usr/bin/env sh'];
|
|
333
|
+
const hasValidShebang = validShebangs.some(shebang => content.startsWith(shebang));
|
|
334
|
+
|
|
335
|
+
if (!hasValidShebang) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Should contain some defensive security patterns
|
|
340
|
+
const securityKeywords = ['credential', 'security', 'validate', 'check', 'prevent'];
|
|
341
|
+
const hasSecurityContent = securityKeywords.some(keyword =>
|
|
342
|
+
content.toLowerCase().includes(keyword)
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return hasSecurityContent;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = HookMetadataService;
|