pumuki-ast-hooks 5.5.51 → 5.5.52
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/docs/MCP_SERVERS.md +16 -2
- package/docs/RELEASE_NOTES.md +34 -0
- package/hooks/git-status-monitor.ts +0 -5
- package/hooks/notify-macos.ts +0 -1
- package/hooks/pre-tool-use-evidence-validator.ts +0 -1
- package/package.json +2 -2
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +48 -0
- package/scripts/hooks-system/application/services/guard/GuardConfig.js +2 -4
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +2 -20
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +9 -205
- package/scripts/hooks-system/application/services/installation/mcp/McpGlobalConfigCleaner.js +49 -0
- package/scripts/hooks-system/application/services/installation/mcp/McpProjectConfigWriter.js +59 -0
- package/scripts/hooks-system/application/services/installation/mcp/McpServerConfigBuilder.js +103 -0
- package/scripts/hooks-system/application/services/monitoring/EvidenceMonitor.js +146 -5
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +1 -13
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +3 -2
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +17 -9
- package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +2 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +137 -27
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDChecks.js +385 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDRules.js +38 -408
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +397 -34
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMChecks.js +408 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMRules.js +36 -442
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenExtractor.js +146 -0
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenParser.js +22 -190
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenRunner.js +62 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +42 -47
- package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +0 -16
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +14 -25
- package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +0 -10
- package/scripts/hooks-system/application/services/installation/HookAssetsInstaller.js +0 -0
- package/scripts/hooks-system/application/services/monitoring/EvidenceRefreshRunner.js +0 -161
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/StagedSwiftFilePreparer.js +0 -59
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftAstRunner.js +0 -51
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftToolchainResolver.js +0 -57
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSAstAnalysisOrchestrator.js +0 -32
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +0 -350
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const env = require('../../../../config/env.js');
|
|
6
|
+
|
|
7
|
+
function slugifyId(input) {
|
|
8
|
+
return String(input || '')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-+|-+$/g, '')
|
|
13
|
+
.slice(0, 48);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class McpServerConfigBuilder {
|
|
17
|
+
constructor(targetRoot, hookSystemRoot, logger = null) {
|
|
18
|
+
this.targetRoot = targetRoot;
|
|
19
|
+
this.hookSystemRoot = hookSystemRoot;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
build() {
|
|
24
|
+
const serverId = this.computeServerIdForRepo(this.targetRoot);
|
|
25
|
+
const entrypoint = this.resolveAutomationEntrypoint();
|
|
26
|
+
const nodePath = this.resolveNodeBinary();
|
|
27
|
+
|
|
28
|
+
const mcpConfig = {
|
|
29
|
+
mcpServers: {
|
|
30
|
+
[serverId]: {
|
|
31
|
+
command: nodePath,
|
|
32
|
+
args: [entrypoint],
|
|
33
|
+
env: {
|
|
34
|
+
REPO_ROOT: this.targetRoot,
|
|
35
|
+
AUTO_COMMIT_ENABLED: 'false',
|
|
36
|
+
AUTO_PUSH_ENABLED: 'false',
|
|
37
|
+
AUTO_PR_ENABLED: 'false'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { serverId, mcpConfig };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
resolveAutomationEntrypoint() {
|
|
47
|
+
const candidates = [
|
|
48
|
+
this.hookSystemRoot
|
|
49
|
+
? path.join(this.hookSystemRoot, 'infrastructure', 'mcp', 'ast-intelligence-automation.js')
|
|
50
|
+
: null,
|
|
51
|
+
path.join(this.targetRoot, 'scripts', 'hooks-system', 'infrastructure', 'mcp', 'ast-intelligence-automation.js')
|
|
52
|
+
].filter(Boolean);
|
|
53
|
+
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.logger?.warn?.('MCP_ENTRYPOINT_CHECK_FAILED', { candidate, error: error.message });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return path.join(this.targetRoot, 'scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
resolveNodeBinary() {
|
|
66
|
+
let nodePath = process.execPath;
|
|
67
|
+
if (nodePath && fs.existsSync(nodePath)) return nodePath;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return execSync('which node', { encoding: 'utf-8' }).trim();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (process.env.DEBUG) {
|
|
73
|
+
process.stderr.write(`[MCP] which node failed: ${error && error.message ? error.message : String(error)}\n`);
|
|
74
|
+
}
|
|
75
|
+
return 'node';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
computeServerIdForRepo(repoRoot) {
|
|
80
|
+
const legacyServerId = 'ast-intelligence-automation';
|
|
81
|
+
const forced = (env.get('MCP_SERVER_ID', '') || '').trim();
|
|
82
|
+
if (forced.length > 0) return forced;
|
|
83
|
+
|
|
84
|
+
const repoName = path.basename(repoRoot || process.cwd());
|
|
85
|
+
const slug = slugifyId(repoName) || 'repo';
|
|
86
|
+
const fp = this.computeRepoFingerprint(repoRoot);
|
|
87
|
+
return `${legacyServerId}-${slug}-${fp}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
computeRepoFingerprint(repoRoot) {
|
|
91
|
+
try {
|
|
92
|
+
const real = fs.realpathSync(repoRoot);
|
|
93
|
+
return crypto.createHash('sha1').update(real).digest('hex').slice(0, 8);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (process.env.DEBUG) {
|
|
96
|
+
process.stderr.write(`[MCP] computeRepoFingerprint fallback: ${error && error.message ? error.message : String(error)}\n`);
|
|
97
|
+
}
|
|
98
|
+
return crypto.createHash('sha1').update(String(repoRoot || '')).digest('hex').slice(0, 8);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = McpServerConfigBuilder;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const {
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { ConfigurationError, DomainError } = require('../../../domain/errors');
|
|
4
5
|
const AuditLogger = require('../logging/AuditLogger');
|
|
5
6
|
|
|
6
7
|
class EvidenceMonitor {
|
|
@@ -13,9 +14,98 @@ class EvidenceMonitor {
|
|
|
13
14
|
this.lastStaleNotification = 0;
|
|
14
15
|
this.pollTimer = null;
|
|
15
16
|
this.evidencePath = path.join(repoRoot, '.AI_EVIDENCE.json');
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
this.tempDir = path.join(repoRoot, '.audit_tmp');
|
|
18
|
+
this.updateScript = this.resolveUpdateEvidenceScript();
|
|
19
|
+
this.refreshInFlight = false;
|
|
20
|
+
this.refreshTimeoutMs = options.refreshTimeoutMs || 120000;
|
|
21
|
+
this.refreshLockFile = path.join(this.tempDir, 'evidence-refresh.lock');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
isPidRunning(pid) {
|
|
25
|
+
if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
acquireRefreshLock() {
|
|
35
|
+
try {
|
|
36
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn('[EvidenceMonitor] Failed to ensure temp dir:', error.message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const fd = fs.openSync(this.refreshLockFile, 'wx');
|
|
43
|
+
const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
|
|
44
|
+
fs.writeFileSync(fd, payload, { encoding: 'utf8' });
|
|
45
|
+
fs.closeSync(fd);
|
|
46
|
+
return { acquired: true };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error && error.code !== 'EEXIST') {
|
|
49
|
+
return { acquired: false, reason: 'error', error };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
|
|
54
|
+
const data = raw ? JSON.parse(raw) : null;
|
|
55
|
+
const lockPid = data && Number(data.pid);
|
|
56
|
+
if (lockPid && this.isPidRunning(lockPid)) {
|
|
57
|
+
return { acquired: false, reason: 'locked', pid: lockPid };
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.warn('[EvidenceMonitor] Failed to read refresh lock file:', error.message);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
fs.unlinkSync(this.refreshLockFile);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn('[EvidenceMonitor] Failed to remove stale refresh lock:', error.message);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const fd = fs.openSync(this.refreshLockFile, 'wx');
|
|
71
|
+
const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
|
|
72
|
+
fs.writeFileSync(fd, payload, { encoding: 'utf8' });
|
|
73
|
+
fs.closeSync(fd);
|
|
74
|
+
return { acquired: true };
|
|
75
|
+
} catch (retryError) {
|
|
76
|
+
return { acquired: false, reason: 'locked', error: retryError };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
releaseRefreshLock() {
|
|
82
|
+
try {
|
|
83
|
+
if (!fs.existsSync(this.refreshLockFile)) return;
|
|
84
|
+
const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
|
|
85
|
+
const data = raw ? JSON.parse(raw) : null;
|
|
86
|
+
const lockPid = data && Number(data.pid);
|
|
87
|
+
if (lockPid === process.pid) {
|
|
88
|
+
fs.unlinkSync(this.refreshLockFile);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn('[EvidenceMonitor] Failed to release refresh lock:', error.message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
resolveUpdateEvidenceScript() {
|
|
96
|
+
const candidates = [
|
|
97
|
+
path.join(this.repoRoot, 'node_modules/@pumuki/ast-intelligence-hooks/bin/update-evidence.sh'),
|
|
98
|
+
path.join(this.repoRoot, 'scripts/hooks-system/bin/update-evidence.sh'),
|
|
99
|
+
path.join(this.repoRoot, 'bin/update-evidence.sh')
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const candidate of candidates) {
|
|
103
|
+
if (fs.existsSync(candidate)) {
|
|
104
|
+
return candidate;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
19
109
|
}
|
|
20
110
|
|
|
21
111
|
isStale() {
|
|
@@ -33,7 +123,58 @@ class EvidenceMonitor {
|
|
|
33
123
|
}
|
|
34
124
|
|
|
35
125
|
async refresh() {
|
|
36
|
-
|
|
126
|
+
if (!this.updateScript) {
|
|
127
|
+
throw new ConfigurationError('Update evidence script not found', 'updateScript');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this.refreshInFlight) {
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const lock = this.acquireRefreshLock();
|
|
135
|
+
if (!lock.acquired) {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.refreshInFlight = true;
|
|
140
|
+
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const child = require('child_process').spawn('bash', [this.updateScript, '--auto', '--refresh-only'], {
|
|
143
|
+
cwd: this.repoRoot,
|
|
144
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let output = '';
|
|
148
|
+
child.stdout.on('data', (data) => {
|
|
149
|
+
output += data.toString();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const timeoutId = setTimeout(() => {
|
|
153
|
+
try {
|
|
154
|
+
child.kill('SIGKILL');
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.warn('[EvidenceMonitor] Failed to kill timed-out refresh process:', error.message);
|
|
157
|
+
}
|
|
158
|
+
}, this.refreshTimeoutMs);
|
|
159
|
+
|
|
160
|
+
child.on('close', (code) => {
|
|
161
|
+
clearTimeout(timeoutId);
|
|
162
|
+
this.refreshInFlight = false;
|
|
163
|
+
this.releaseRefreshLock();
|
|
164
|
+
if (code === 0) {
|
|
165
|
+
resolve(output);
|
|
166
|
+
} else {
|
|
167
|
+
reject(new DomainError(`Evidence refresh failed with code ${code}`, 'EVIDENCE_REFRESH_FAILED'));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
child.on('error', (err) => {
|
|
172
|
+
clearTimeout(timeoutId);
|
|
173
|
+
this.refreshInFlight = false;
|
|
174
|
+
this.releaseRefreshLock();
|
|
175
|
+
reject(err);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
37
178
|
}
|
|
38
179
|
|
|
39
180
|
startPolling(onStale, onRefreshed) {
|
|
@@ -45,18 +45,6 @@ function getRepoRoot() {
|
|
|
45
45
|
*/
|
|
46
46
|
function shouldIgnore(file) {
|
|
47
47
|
const p = file.replace(/\\/g, "/");
|
|
48
|
-
try {
|
|
49
|
-
const configPaths = loadExclusions()?.exclusions?.paths;
|
|
50
|
-
if (configPaths && typeof configPaths === 'object') {
|
|
51
|
-
for (const [key, enabled] of Object.entries(configPaths)) {
|
|
52
|
-
if (enabled && p.includes(key)) return true;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (process.env.DEBUG) {
|
|
57
|
-
console.debug(`[ast-core] Failed to load exclusions for shouldIgnore: ${error.message || String(error)}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
48
|
if (p.includes("node_modules/")) return true;
|
|
61
49
|
if (p.includes("/.next/")) return true;
|
|
62
50
|
if (p.includes("/dist/")) return true;
|
|
@@ -137,7 +125,7 @@ let exclusionsConfig = null;
|
|
|
137
125
|
function loadExclusions() {
|
|
138
126
|
if (exclusionsConfig) return exclusionsConfig;
|
|
139
127
|
try {
|
|
140
|
-
const configPath = path.join(
|
|
128
|
+
const configPath = path.join(__dirname, '../../config/ast-exclusions.json');
|
|
141
129
|
if (fs.existsSync(configPath)) {
|
|
142
130
|
exclusionsConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
143
131
|
}
|
|
@@ -22,7 +22,7 @@ function formatLocalTimestamp(date = new Date()) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const astModulesPath = __dirname;
|
|
25
|
-
const { createProject, platformOf, mapToLevel
|
|
25
|
+
const { createProject, platformOf, mapToLevel } = require(path.join(astModulesPath, "ast-core"));
|
|
26
26
|
const MacOSNotificationAdapter = require(path.join(__dirname, '../adapters/MacOSNotificationAdapter'));
|
|
27
27
|
const { runBackendIntelligence } = require(path.join(astModulesPath, "backend/ast-backend"));
|
|
28
28
|
const { runFrontendIntelligence } = require(path.join(astModulesPath, "frontend/ast-frontend"));
|
|
@@ -138,6 +138,8 @@ function runProjectHardcodedThresholdAudit(root, allFiles, findings) {
|
|
|
138
138
|
if (p.includes('/build/')) return true;
|
|
139
139
|
if (p.includes('/coverage/')) return true;
|
|
140
140
|
if (p.includes('/.audit_tmp/')) return true;
|
|
141
|
+
if (p.includes('/infrastructure/ast/')) return true;
|
|
142
|
+
if (p.includes('/scripts/hooks-system/')) return true;
|
|
141
143
|
return false;
|
|
142
144
|
};
|
|
143
145
|
|
|
@@ -704,7 +706,6 @@ function listSourceFiles(root) {
|
|
|
704
706
|
*/
|
|
705
707
|
function shouldIgnore(file) {
|
|
706
708
|
const p = file.replace(/\\/g, "/");
|
|
707
|
-
if (typeof coreShouldIgnore === 'function' && coreShouldIgnore(p)) return true;
|
|
708
709
|
if (p.includes("node_modules/")) return true;
|
|
709
710
|
if (p.includes("/.cursor/")) return true;
|
|
710
711
|
if (/\.bak/i.test(p)) return true;
|
|
@@ -126,10 +126,13 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
// NO excluir archivos AST - la librería debe auto-auditarse
|
|
129
|
+
if (isTestFile(filePath)) return;
|
|
130
|
+
|
|
129
131
|
sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
130
132
|
const className = cls.getName() || '';
|
|
131
133
|
const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
|
|
132
|
-
|
|
134
|
+
const isTestClass = /Spec$|Test$|Mock/.test(className);
|
|
135
|
+
if (isValueObject || isTestClass) return;
|
|
133
136
|
|
|
134
137
|
const methodsCount = cls.getMethods().length;
|
|
135
138
|
const propertiesCount = cls.getProperties().length;
|
|
@@ -207,6 +210,9 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
207
210
|
|
|
208
211
|
if (platformOf(filePath) !== "backend") return;
|
|
209
212
|
|
|
213
|
+
const normalizedBackendPath = filePath.replace(/\\/g, '/');
|
|
214
|
+
const isRealBackendAppFile = normalizedBackendPath.includes('/apps/backend/') || normalizedBackendPath.includes('apps/backend/');
|
|
215
|
+
|
|
210
216
|
// NO excluir archivos AST - la librería debe auto-auditarse para detectar God classes masivas
|
|
211
217
|
|
|
212
218
|
const fullText = sf.getFullText();
|
|
@@ -272,7 +278,7 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
272
278
|
fullTextUpper.includes("ConfigService") ||
|
|
273
279
|
usesEnvHelper;
|
|
274
280
|
const hasConfigUsage = /process\.env\b|ConfigService|env\.get(Bool|Number)?\(|\bconfig\s*[\.\[]/i.test(sf.getFullText());
|
|
275
|
-
if (!hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
|
|
281
|
+
if (isRealBackendAppFile && !hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
|
|
276
282
|
pushFinding("backend.config.missing_env_separation", "info", sf, sf, "Missing environment-specific configuration - consider NODE_ENV or ConfigService", findings);
|
|
277
283
|
}
|
|
278
284
|
|
|
@@ -287,6 +293,10 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
287
293
|
analyzeGodClasses(sf, findings, { SyntaxKind, pushFinding, godClassBaseline });
|
|
288
294
|
}
|
|
289
295
|
|
|
296
|
+
if (!isRealBackendAppFile) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
290
300
|
sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
291
301
|
const name = cls.getName();
|
|
292
302
|
if (name && /Entity|Model|Domain/.test(name)) {
|
|
@@ -349,16 +359,12 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
349
359
|
}
|
|
350
360
|
});
|
|
351
361
|
|
|
352
|
-
const filePathNormalizedForMetrics = filePath.replace(/\\/g, "/");
|
|
353
|
-
const filePathNormalizedForMetricsLower = filePathNormalizedForMetrics.toLowerCase();
|
|
354
|
-
const isInternalAstToolingFileForMetrics = filePathNormalizedForMetricsLower.includes("/infrastructure/ast/") || filePathNormalizedForMetricsLower.includes("infrastructure/ast/") || filePathNormalizedForMetricsLower.includes("/scripts/hooks-system/infrastructure/ast/");
|
|
355
|
-
|
|
356
362
|
const fullTextLower = fullText.toLowerCase();
|
|
357
363
|
const hasMetrics = fullTextLower.includes("micrometer") || fullTextLower.includes("prometheus") ||
|
|
358
364
|
fullTextLower.includes("actuator") || fullTextLower.includes("metrics");
|
|
359
365
|
const looksLikeServiceOrController = fullTextLower.includes("controller") || fullTextLower.includes("service");
|
|
360
366
|
|
|
361
|
-
if (
|
|
367
|
+
if (isRealBackendAppFile && !hasMetrics && looksLikeServiceOrController) {
|
|
362
368
|
pushFinding("backend.metrics.missing_prometheus", "info", sf, sf, "Missing application metrics - consider Spring Boot Actuator or Micrometer for monitoring", findings);
|
|
363
369
|
}
|
|
364
370
|
|
|
@@ -478,7 +484,7 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
478
484
|
sf.getDescendantsOfKind(SyntaxKind.ThrowStatement).forEach((throwStmt) => {
|
|
479
485
|
const expr = throwStmt.getExpression();
|
|
480
486
|
if (!expr) return;
|
|
481
|
-
const exprText =
|
|
487
|
+
const exprText = throwStmt.getExpression().getText();
|
|
482
488
|
if (exprText.includes("Error(") || exprText.includes("Exception(")) {
|
|
483
489
|
const isCustom = exprText.includes("Exception") &&
|
|
484
490
|
(exprText.includes("Validation") ||
|
|
@@ -486,7 +492,9 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
486
492
|
exprText.includes("Unauthorized") ||
|
|
487
493
|
exprText.includes("Forbidden"));
|
|
488
494
|
if (!isCustom) {
|
|
489
|
-
|
|
495
|
+
if (isRealBackendAppFile) {
|
|
496
|
+
pushFinding("backend.error.custom_exceptions", "info", sf, throwStmt, "Generic Error/Exception thrown - create custom exception classes for better error handling", findings);
|
|
497
|
+
}
|
|
490
498
|
}
|
|
491
499
|
}
|
|
492
500
|
});
|
|
@@ -7,7 +7,8 @@ function analyzeGodClasses(sourceFile, findings, { SyntaxKind, pushFinding, godC
|
|
|
7
7
|
sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
8
8
|
const className = cls.getName() || '';
|
|
9
9
|
const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
|
|
10
|
-
|
|
10
|
+
const isTestClass = /Spec$|Test$|Mock/.test(className);
|
|
11
|
+
if (isValueObject || isTestClass) return;
|
|
11
12
|
|
|
12
13
|
const methodsCount = cls.getMethods().length;
|
|
13
14
|
const propertiesCount = cls.getProperties().length;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const env = require(path.join(__dirname, '../../../../config/env'));
|
|
5
6
|
const {
|
|
6
7
|
resetCollections,
|
|
@@ -8,21 +9,13 @@ const {
|
|
|
8
9
|
analyzeCollectedNodes,
|
|
9
10
|
finalizeGodClassDetection,
|
|
10
11
|
} = require('../detectors/ios-ast-intelligent-strategies');
|
|
11
|
-
const { SwiftToolchainResolver } = require('./SwiftToolchainResolver');
|
|
12
|
-
const { StagedSwiftFilePreparer } = require('./StagedSwiftFilePreparer');
|
|
13
|
-
const { SwiftAstRunner } = require('./SwiftAstRunner');
|
|
14
|
-
const { iOSAstAnalysisOrchestrator } = require('./iOSAstAnalysisOrchestrator');
|
|
15
12
|
|
|
16
13
|
class iOSASTIntelligentAnalyzer {
|
|
17
14
|
constructor(findings) {
|
|
18
15
|
this.findings = findings;
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
22
|
-
this.swiftSyntaxPath = this.toolchain.swiftSyntaxPath;
|
|
23
|
-
this.hasSwiftSyntax = Boolean(this.swiftSyntaxPath);
|
|
24
|
-
this.swiftRunner = new SwiftAstRunner({ toolchain: this.toolchain });
|
|
25
|
-
this.analysisOrchestrator = new iOSAstAnalysisOrchestrator();
|
|
16
|
+
this.sourceKittenPath = '/opt/homebrew/bin/sourcekitten';
|
|
17
|
+
this.isAvailable = this.checkSourceKitten();
|
|
18
|
+
this.hasSwiftSyntax = this.checkSwiftSyntax();
|
|
26
19
|
this.fileContent = '';
|
|
27
20
|
this.currentFilePath = '';
|
|
28
21
|
this.godClassCandidates = [];
|
|
@@ -36,6 +29,99 @@ class iOSASTIntelligentAnalyzer {
|
|
|
36
29
|
this.closures = [];
|
|
37
30
|
}
|
|
38
31
|
|
|
32
|
+
resolveAuditTmpDir(repoRoot) {
|
|
33
|
+
const configured = String(env.get('AUDIT_TMP', '') || '').trim();
|
|
34
|
+
if (configured.length > 0) {
|
|
35
|
+
return path.isAbsolute(configured) ? configured : path.join(repoRoot, configured);
|
|
36
|
+
}
|
|
37
|
+
return path.join(repoRoot, '.audit_tmp');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
safeTempFilePath(repoRoot, displayPath) {
|
|
41
|
+
const tmpDir = this.resolveAuditTmpDir(repoRoot);
|
|
42
|
+
const hash = crypto.createHash('sha1').update(String(displayPath)).digest('hex').slice(0, 10);
|
|
43
|
+
const base = path.basename(displayPath, '.swift');
|
|
44
|
+
const filename = `${base}.${hash}.staged.swift`;
|
|
45
|
+
return path.join(tmpDir, filename);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
readStagedFileContent(repoRoot, relPath) {
|
|
49
|
+
try {
|
|
50
|
+
return execSync(`git show :"${relPath}"`, { encoding: 'utf8', cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (process.env.DEBUG) {
|
|
53
|
+
console.debug(`[iOSASTIntelligentAnalyzer] Failed to read staged file ${relPath}: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
checkSourceKitten() {
|
|
60
|
+
try {
|
|
61
|
+
execSync(`${this.sourceKittenPath} version`, { stdio: 'pipe' });
|
|
62
|
+
return true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (process.env.DEBUG) {
|
|
65
|
+
console.debug(`[iOSASTIntelligentAnalyzer] SourceKitten not available at ${this.sourceKittenPath}: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
checkSwiftSyntax() {
|
|
72
|
+
const projectRoot = require('child_process')
|
|
73
|
+
.execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' })
|
|
74
|
+
.trim();
|
|
75
|
+
|
|
76
|
+
const possiblePaths = [
|
|
77
|
+
path.join(__dirname, '../../../../../CustomLintRules/.build/debug/swift-ast-analyzer'),
|
|
78
|
+
path.join(projectRoot, 'CustomLintRules/.build/debug/swift-ast-analyzer'),
|
|
79
|
+
path.join(projectRoot, '.build/debug/swift-ast-analyzer')
|
|
80
|
+
];
|
|
81
|
+
for (const p of possiblePaths) {
|
|
82
|
+
if (fs.existsSync(p)) {
|
|
83
|
+
this.swiftSyntaxPath = p;
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
analyzeWithSwiftSyntax(filePath, displayPath = null) {
|
|
91
|
+
if (!this.swiftSyntaxPath) return;
|
|
92
|
+
try {
|
|
93
|
+
const result = execSync(`"${this.swiftSyntaxPath}" "${filePath}"`, {
|
|
94
|
+
encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe']
|
|
95
|
+
});
|
|
96
|
+
const violations = JSON.parse(result);
|
|
97
|
+
for (const v of violations) {
|
|
98
|
+
const reportedPath = displayPath || filePath;
|
|
99
|
+
this.findings.push({
|
|
100
|
+
ruleId: v.ruleId, severity: v.severity, filePath: reportedPath,
|
|
101
|
+
line: v.line, column: v.column, message: v.message
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('[iOSASTIntelligentAnalyzer] Error parsing file:', error.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
parseFile(filePath) {
|
|
110
|
+
if (!this.isAvailable) return null;
|
|
111
|
+
try {
|
|
112
|
+
const result = execSync(
|
|
113
|
+
`${this.sourceKittenPath} structure --file "${filePath}"`,
|
|
114
|
+
{ encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
115
|
+
);
|
|
116
|
+
return JSON.parse(result);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (process.env.DEBUG) {
|
|
119
|
+
console.debug(`[iOSASTIntelligentAnalyzer] SourceKitten parse failed for ${filePath}: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
39
125
|
analyzeFile(filePath, options = {}) {
|
|
40
126
|
if (!filePath || !String(filePath).endsWith('.swift')) return;
|
|
41
127
|
|
|
@@ -43,29 +129,53 @@ class iOSASTIntelligentAnalyzer {
|
|
|
43
129
|
const displayPath = options.displayPath || filePath;
|
|
44
130
|
const stagedRelPath = options.stagedRelPath || null;
|
|
45
131
|
const stagingOnly = env.get('STAGING_ONLY_MODE', '0') === '1';
|
|
46
|
-
const auditTmpDir = this.toolchain.resolveAuditTmpDir(repoRoot);
|
|
47
|
-
const stagedPreparer = new StagedSwiftFilePreparer({ repoRoot, auditTmpDir });
|
|
48
132
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
133
|
+
let parsePath = filePath;
|
|
134
|
+
let contentOverride = null;
|
|
135
|
+
|
|
136
|
+
if (stagingOnly && stagedRelPath) {
|
|
137
|
+
const stagedContent = this.readStagedFileContent(repoRoot, stagedRelPath);
|
|
138
|
+
if (typeof stagedContent === 'string') {
|
|
139
|
+
const tmpPath = this.safeTempFilePath(repoRoot, stagedRelPath);
|
|
140
|
+
try {
|
|
141
|
+
fs.mkdirSync(path.dirname(tmpPath), { recursive: true });
|
|
142
|
+
fs.writeFileSync(tmpPath, stagedContent, 'utf8');
|
|
143
|
+
parsePath = tmpPath;
|
|
144
|
+
contentOverride = stagedContent;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (process.env.DEBUG) {
|
|
147
|
+
console.debug(`[iOSASTIntelligentAnalyzer] Failed to write temp staged file ${tmpPath}: ${error.message}`);
|
|
148
|
+
}
|
|
149
|
+
// Fall back to working tree file
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
54
153
|
|
|
55
154
|
if (this.hasSwiftSyntax) {
|
|
56
|
-
this.
|
|
155
|
+
this.analyzeWithSwiftSyntax(parsePath, displayPath);
|
|
57
156
|
}
|
|
58
157
|
|
|
59
|
-
const ast = this.
|
|
158
|
+
const ast = this.parseFile(parsePath);
|
|
60
159
|
if (!ast) return;
|
|
61
160
|
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
161
|
+
this.currentFilePath = displayPath;
|
|
162
|
+
resetCollections(this);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
this.fileContent = typeof contentOverride === 'string'
|
|
166
|
+
? contentOverride
|
|
167
|
+
: fs.readFileSync(parsePath, 'utf8');
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (process.env.DEBUG) {
|
|
170
|
+
console.debug(`[iOSASTIntelligentAnalyzer] Failed to read file content ${parsePath}: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
this.fileContent = '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const substructure = ast['key.substructure'] || [];
|
|
176
|
+
|
|
177
|
+
collectAllNodes(this, substructure, null);
|
|
178
|
+
analyzeCollectedNodes(this, displayPath);
|
|
69
179
|
}
|
|
70
180
|
|
|
71
181
|
safeStringify(obj) {
|