pumuki-ast-hooks 5.3.30 → 5.4.0
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/VIOLATIONS_RESOLUTION_PLAN.md +10 -10
- package/package.json +2 -2
- package/scripts/hooks-system/application/CompositionRoot.js +57 -282
- package/scripts/hooks-system/application/factories/AdapterFactory.js +58 -0
- package/scripts/hooks-system/application/factories/MonitorFactory.js +104 -0
- package/scripts/hooks-system/application/factories/ServiceFactory.js +153 -0
- package/scripts/hooks-system/application/services/RealtimeGuardService.js +49 -246
- package/scripts/hooks-system/application/services/guard/EvidenceManager.js +153 -0
- package/scripts/hooks-system/application/services/guard/GitTreeManager.js +129 -0
- package/scripts/hooks-system/application/services/guard/GuardNotifier.js +54 -0
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +3 -2
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +33 -29
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +80 -10
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +45 -11
- package/scripts/hooks-system/infrastructure/ast/ios/native-bridge.js +39 -2
- package/scripts/hooks-system/.hook-system/config.json +0 -8
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { recordMetric } = require('../../../infrastructure/telemetry/metrics-logger');
|
|
4
|
+
const env = require('../../../config/env.js');
|
|
5
|
+
|
|
6
|
+
class EvidenceManager {
|
|
7
|
+
constructor(evidencePath, notifier, auditLogger) {
|
|
8
|
+
this.evidencePath = evidencePath;
|
|
9
|
+
this.notifier = notifier;
|
|
10
|
+
this.auditLogger = auditLogger;
|
|
11
|
+
this.staleThresholdMs = env.getNumber('HOOK_GUARD_EVIDENCE_STALE_THRESHOLD', 60000);
|
|
12
|
+
this.reminderIntervalMs = env.getNumber('HOOK_GUARD_EVIDENCE_REMINDER_INTERVAL', 60000);
|
|
13
|
+
this.inactivityGraceMs = env.getNumber('HOOK_GUARD_INACTIVITY_GRACE_MS', 120000);
|
|
14
|
+
this.pollIntervalMs = env.getNumber('HOOK_GUARD_EVIDENCE_POLL_INTERVAL', 30000);
|
|
15
|
+
this.pollTimer = null;
|
|
16
|
+
this.lastStaleNotification = 0;
|
|
17
|
+
this.lastUserActivityAt = 0;
|
|
18
|
+
this.autoRefreshCooldownMs = env.getNumber('HOOK_GUARD_EVIDENCE_AUTO_REFRESH_COOLDOWN', 180000);
|
|
19
|
+
this.lastAutoRefresh = 0;
|
|
20
|
+
this.autoRefreshInFlight = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
startPolling(onStale, onRefresh) {
|
|
24
|
+
this.onStale = onStale;
|
|
25
|
+
this.onRefresh = onRefresh;
|
|
26
|
+
|
|
27
|
+
if (this.pollTimer) {
|
|
28
|
+
clearInterval(this.pollTimer);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (this.pollIntervalMs <= 0) return;
|
|
32
|
+
|
|
33
|
+
this.pollTimer = setInterval(() => {
|
|
34
|
+
this.evaluateEvidenceAge('polling');
|
|
35
|
+
}, this.pollIntervalMs);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
stopPolling() {
|
|
39
|
+
if (this.pollTimer) {
|
|
40
|
+
clearInterval(this.pollTimer);
|
|
41
|
+
this.pollTimer = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
readEvidenceTimestamp() {
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.existsSync(this.evidencePath)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const raw = fs.readFileSync(this.evidencePath, 'utf8');
|
|
51
|
+
const json = JSON.parse(raw);
|
|
52
|
+
const ts = json?.timestamp;
|
|
53
|
+
if (!ts) return null;
|
|
54
|
+
const ms = new Date(ts).getTime();
|
|
55
|
+
if (Number.isNaN(ms)) return null;
|
|
56
|
+
return ms;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const msg = error && error.message ? error.message : String(error);
|
|
59
|
+
this.notifier.appendDebugLog(`EVIDENCE_TIMESTAMP_ERROR|${msg}`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
evaluateEvidenceAge(source = 'manual', notifyFresh = false) {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const timestamp = this.readEvidenceTimestamp();
|
|
67
|
+
if (!timestamp) return;
|
|
68
|
+
|
|
69
|
+
const ageMs = now - timestamp;
|
|
70
|
+
const isStale = ageMs > this.staleThresholdMs;
|
|
71
|
+
const isRecentlyActive = this.lastUserActivityAt && (now - this.lastUserActivityAt) < this.inactivityGraceMs;
|
|
72
|
+
|
|
73
|
+
if (isStale && !isRecentlyActive) {
|
|
74
|
+
this.triggerStaleAlert(source, ageMs);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (notifyFresh && this.lastStaleNotification > 0 && !isStale) {
|
|
79
|
+
this.notifier.notify('Evidence updated; back within SLA.', 'info');
|
|
80
|
+
this.lastStaleNotification = 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
triggerStaleAlert(source, ageMs) {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
if (this.lastStaleNotification && (now - this.lastStaleNotification) < this.reminderIntervalMs) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.lastStaleNotification = now;
|
|
91
|
+
const ageSec = Math.floor(ageMs / 1000);
|
|
92
|
+
this.notifier.notify(`Evidence has been stale for ${ageSec}s (source: ${source}).`, 'warn', { forceDialog: true });
|
|
93
|
+
this.auditLogger.record({
|
|
94
|
+
action: 'guard.evidence.stale',
|
|
95
|
+
resource: 'evidence',
|
|
96
|
+
status: 'warning',
|
|
97
|
+
meta: { ageSec, source }
|
|
98
|
+
});
|
|
99
|
+
recordMetric({ hook: 'evidence', status: 'stale', ageSec, source });
|
|
100
|
+
this.attemptAutoRefresh('stale');
|
|
101
|
+
|
|
102
|
+
if (this.onStale) this.onStale();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async attemptAutoRefresh(reason = 'manual') {
|
|
106
|
+
if (!env.getBool('HOOK_GUARD_AUTO_REFRESH', false)) return;
|
|
107
|
+
|
|
108
|
+
const updateScriptCandidates = [
|
|
109
|
+
path.join(process.cwd(), 'scripts/hooks-system/bin/update-evidence.sh'),
|
|
110
|
+
path.join(process.cwd(), 'node_modules/@pumuki/ast-intelligence-hooks/bin/update-evidence.sh'),
|
|
111
|
+
path.join(process.cwd(), 'bin/update-evidence.sh')
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const updateScript = updateScriptCandidates.find(p => fs.existsSync(p));
|
|
115
|
+
if (!updateScript) return;
|
|
116
|
+
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const ts = this.readEvidenceTimestamp();
|
|
119
|
+
if (ts && (now - ts) <= this.staleThresholdMs) return;
|
|
120
|
+
|
|
121
|
+
if (this.lastAutoRefresh && (now - this.lastAutoRefresh) < this.autoRefreshCooldownMs) return;
|
|
122
|
+
|
|
123
|
+
if (this.autoRefreshInFlight) return;
|
|
124
|
+
|
|
125
|
+
this.autoRefreshInFlight = true;
|
|
126
|
+
try {
|
|
127
|
+
await this.runDirectEvidenceRefresh(reason);
|
|
128
|
+
this.lastAutoRefresh = now;
|
|
129
|
+
this.auditLogger.record({
|
|
130
|
+
action: 'guard.evidence.auto_refresh',
|
|
131
|
+
resource: 'evidence',
|
|
132
|
+
status: 'success',
|
|
133
|
+
meta: { reason }
|
|
134
|
+
});
|
|
135
|
+
recordMetric({ hook: 'evidence', status: 'auto_refresh_success', reason });
|
|
136
|
+
|
|
137
|
+
if (this.onRefresh) this.onRefresh();
|
|
138
|
+
} finally {
|
|
139
|
+
this.autoRefreshInFlight = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async runDirectEvidenceRefresh(_reason) {
|
|
144
|
+
// Specific implementation if needed
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
updateUserActivity() {
|
|
149
|
+
this.lastUserActivityAt = Date.now();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = EvidenceManager;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const { getGitTreeState, isTreeBeyondLimit } = require('../GitTreeState');
|
|
2
|
+
const { recordMetric } = require('../../../infrastructure/telemetry/metrics-logger');
|
|
3
|
+
const env = require('../../../config/env.js');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
class GitTreeManager {
|
|
8
|
+
constructor(notifier, auditLogger) {
|
|
9
|
+
this.notifier = notifier;
|
|
10
|
+
this.auditLogger = auditLogger;
|
|
11
|
+
this.gitTreeStagedThreshold = env.getNumber('HOOK_GUARD_DIRTY_TREE_STAGED_LIMIT', 10);
|
|
12
|
+
this.gitTreeUnstagedThreshold = env.getNumber('HOOK_GUARD_DIRTY_TREE_UNSTAGED_LIMIT', 15);
|
|
13
|
+
this.gitTreeTotalThreshold = env.getNumber('HOOK_GUARD_DIRTY_TREE_TOTAL_LIMIT', 20);
|
|
14
|
+
this.gitTreeCheckIntervalMs = env.getNumber('HOOK_GUARD_DIRTY_TREE_INTERVAL', 60000);
|
|
15
|
+
this.gitTreeReminderMs = env.getNumber('HOOK_GUARD_DIRTY_TREE_REMINDER', 300000);
|
|
16
|
+
this.gitTreeTimer = null;
|
|
17
|
+
this.lastDirtyTreeNotification = 0;
|
|
18
|
+
this.dirtyTreeActive = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
startMonitoring(onStateChange) {
|
|
22
|
+
this.onStateChange = onStateChange;
|
|
23
|
+
|
|
24
|
+
if (env.getBool('HOOK_GUARD_DIRTY_TREE_DISABLED', false)) return;
|
|
25
|
+
|
|
26
|
+
if (this.gitTreeTimer) {
|
|
27
|
+
clearInterval(this.gitTreeTimer);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const thresholdsValid = this.gitTreeStagedThreshold > 0 || this.gitTreeUnstagedThreshold > 0 || this.gitTreeTotalThreshold > 0;
|
|
31
|
+
if (!thresholdsValid || this.gitTreeCheckIntervalMs <= 0) return;
|
|
32
|
+
|
|
33
|
+
this.evaluateGitTree();
|
|
34
|
+
this.gitTreeTimer = setInterval(() => {
|
|
35
|
+
this.evaluateGitTree();
|
|
36
|
+
}, this.gitTreeCheckIntervalMs);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
stopMonitoring() {
|
|
40
|
+
if (this.gitTreeTimer) {
|
|
41
|
+
clearInterval(this.gitTreeTimer);
|
|
42
|
+
this.gitTreeTimer = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async evaluateGitTree() {
|
|
47
|
+
try {
|
|
48
|
+
const state = getGitTreeState();
|
|
49
|
+
const limits = {
|
|
50
|
+
stagedLimit: this.gitTreeStagedThreshold,
|
|
51
|
+
unstagedLimit: this.gitTreeUnstagedThreshold,
|
|
52
|
+
totalLimit: this.gitTreeTotalThreshold
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (isTreeBeyondLimit(state, limits)) {
|
|
56
|
+
this.handleDirtyTree(state, limits);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.resolveDirtyTree(state, limits);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.notifier.appendDebugLog(`DIRTY_TREE_ERROR|${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
resolveDirtyTree(_state, _limits) {
|
|
66
|
+
this.dirtyTreeActive = false;
|
|
67
|
+
this.notifier.notify('✅ Git tree is clean', 'success');
|
|
68
|
+
this.auditLogger.record({
|
|
69
|
+
action: 'guard.git_tree.clean',
|
|
70
|
+
resource: 'git_tree',
|
|
71
|
+
status: 'success'
|
|
72
|
+
});
|
|
73
|
+
recordMetric({ hook: 'git_tree', status: 'clean' });
|
|
74
|
+
|
|
75
|
+
if (this.onStateChange) {
|
|
76
|
+
this.onStateChange({ isBeyondLimit: false, ..._state });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
handleDirtyTree(_state, limitOrLimits) {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const limits = typeof limitOrLimits === 'number'
|
|
83
|
+
? { totalLimit: limitOrLimits }
|
|
84
|
+
: (limitOrLimits || {});
|
|
85
|
+
|
|
86
|
+
if (this.lastDirtyTreeNotification && (now - this.lastDirtyTreeNotification) < this.gitTreeReminderMs) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.lastDirtyTreeNotification = now;
|
|
91
|
+
this.dirtyTreeActive = true;
|
|
92
|
+
const message = `Git tree has too many files: ${_state.total} total (${_state.staged} staged, ${_state.unstaged} unstaged)`;
|
|
93
|
+
this.notifier.notify(message, 'error', { forceDialog: true, ...limits });
|
|
94
|
+
this.auditLogger.record({
|
|
95
|
+
action: 'guard.git_tree.dirty',
|
|
96
|
+
resource: 'git_tree',
|
|
97
|
+
status: 'warning',
|
|
98
|
+
meta: { total: _state.total, staged: _state.staged, unstaged: _state.unstaged }
|
|
99
|
+
});
|
|
100
|
+
recordMetric({ hook: 'git_tree', status: 'dirty', total: _state.total, staged: _state.staged, unstaged: _state.unstaged });
|
|
101
|
+
this.persistDirtyTreeState();
|
|
102
|
+
|
|
103
|
+
if (this.onStateChange) {
|
|
104
|
+
this.onStateChange({ isBeyondLimit: true, ..._state });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
persistDirtyTreeState() {
|
|
109
|
+
const persistPath = path.join(process.cwd(), '.audit_tmp', 'dirty-tree-state.json');
|
|
110
|
+
const state = {
|
|
111
|
+
timestamp: new Date().toISOString(),
|
|
112
|
+
lastNotification: this.lastDirtyTreeNotification,
|
|
113
|
+
isActive: this.dirtyTreeActive,
|
|
114
|
+
thresholds: {
|
|
115
|
+
staged: this.gitTreeStagedThreshold,
|
|
116
|
+
unstaged: this.gitTreeUnstagedThreshold,
|
|
117
|
+
total: this.gitTreeTotalThreshold
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
fs.writeFileSync(persistPath, JSON.stringify(state, null, 2));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.notifier.appendDebugLog(`PERSIST_DIRTY_TREE_ERROR|${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = GitTreeManager;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class GuardNotifier {
|
|
5
|
+
constructor(logger, notificationService, notifier = null, notificationsEnabled = true) {
|
|
6
|
+
this.logger = logger;
|
|
7
|
+
this.notificationService = notificationService;
|
|
8
|
+
this.notifier = typeof notifier === 'function' ? notifier : null;
|
|
9
|
+
this.notificationsEnabled = notificationsEnabled;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setDebugLogPath(debugLogPath) {
|
|
13
|
+
this.debugLogPath = debugLogPath;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
notify(message, level = 'info', options = {}) {
|
|
17
|
+
const { forceDialog = false, ...metadata } = options;
|
|
18
|
+
this._appendDebugLog(`NOTIFY|${level}|${forceDialog ? 'force-dialog|' : ''}${message}`);
|
|
19
|
+
|
|
20
|
+
if (this.notifier && this.notificationsEnabled) {
|
|
21
|
+
try {
|
|
22
|
+
this.notifier(message, level, { ...metadata, forceDialog });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const msg = error && error.message ? error.message : String(error);
|
|
25
|
+
this._appendDebugLog(`NOTIFIER_ERROR|${msg}`);
|
|
26
|
+
this.logger?.debug?.('REALTIME_GUARD_NOTIFIER_ERROR', { error: msg });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (this.notificationService) {
|
|
31
|
+
this.notificationService.enqueue({
|
|
32
|
+
message,
|
|
33
|
+
level,
|
|
34
|
+
metadata: { ...metadata, forceDialog }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_appendDebugLog(entry) {
|
|
40
|
+
if (!this.debugLogPath) return;
|
|
41
|
+
try {
|
|
42
|
+
const timestamp = new Date().toISOString();
|
|
43
|
+
fs.appendFileSync(this.debugLogPath, `[${timestamp}] ${entry}\n`);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('[GuardNotifier] Failed to write debug log:', error.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
appendDebugLog(entry) {
|
|
50
|
+
this._appendDebugLog(entry);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = GuardNotifier;
|
|
@@ -61,8 +61,9 @@ class McpConfigurator {
|
|
|
61
61
|
for (const c of candidates) {
|
|
62
62
|
try {
|
|
63
63
|
if (fs.existsSync(c)) return c;
|
|
64
|
-
} catch {
|
|
65
|
-
//
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Log error but continue checking other candidates
|
|
66
|
+
console.warn(`Failed to check path ${c}:`, error.message);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -28,6 +28,9 @@ async function runASTIntelligence() {
|
|
|
28
28
|
const { getRepoRoot } = require('./ast-core');
|
|
29
29
|
const root = getRepoRoot();
|
|
30
30
|
|
|
31
|
+
const stagingOnlyMode = env.get('STAGING_ONLY_MODE', '0') === '1';
|
|
32
|
+
const stagedRelFiles = stagingOnlyMode ? getStagedFilesRel(root) : [];
|
|
33
|
+
|
|
31
34
|
const allFiles = listSourceFiles(root);
|
|
32
35
|
|
|
33
36
|
const project = createProject(allFiles);
|
|
@@ -58,7 +61,7 @@ async function runASTIntelligence() {
|
|
|
58
61
|
await runPlatformAnalysis(project, findings, context);
|
|
59
62
|
|
|
60
63
|
// Generate output
|
|
61
|
-
generateOutput(findings, context, project, root);
|
|
64
|
+
generateOutput(findings, { ...context, stagingOnlyMode, stagedFiles: stagedRelFiles }, project, root);
|
|
62
65
|
|
|
63
66
|
} catch (error) {
|
|
64
67
|
console.error("AST Intelligence Error:", error.message);
|
|
@@ -69,6 +72,23 @@ async function runASTIntelligence() {
|
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
function getStagedFilesRel(root) {
|
|
76
|
+
try {
|
|
77
|
+
const { execSync } = require('child_process');
|
|
78
|
+
return execSync('git diff --cached --name-only --diff-filter=ACM', {
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
cwd: root,
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
82
|
+
})
|
|
83
|
+
.trim()
|
|
84
|
+
.split('\n')
|
|
85
|
+
.map(s => s.trim())
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
function runProjectHardcodedThresholdAudit(root, allFiles, findings) {
|
|
73
93
|
if (env.get('AST_INSIGHTS', '0') !== '1') return;
|
|
74
94
|
|
|
@@ -368,36 +388,18 @@ function generateOutput(findings, context, project, root) {
|
|
|
368
388
|
|
|
369
389
|
// Top violations
|
|
370
390
|
const grouped = {};
|
|
371
|
-
const levelRank = { LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 };
|
|
372
|
-
const emojiForLevel = (level) => (level === 'CRITICAL' || level === 'HIGH' ? '🔴' : '🔵');
|
|
373
|
-
|
|
374
391
|
findings.forEach(f => {
|
|
375
|
-
|
|
376
|
-
const level = mapToLevel(f.severity);
|
|
377
|
-
if (!grouped[f.ruleId]) {
|
|
378
|
-
grouped[f.ruleId] = { count: 0, worstLevel: level };
|
|
379
|
-
}
|
|
380
|
-
grouped[f.ruleId].count += 1;
|
|
381
|
-
if ((levelRank[level] || 0) > (levelRank[grouped[f.ruleId].worstLevel] || 0)) {
|
|
382
|
-
grouped[f.ruleId].worstLevel = level;
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
const entries = Object.entries(grouped)
|
|
387
|
-
.map(([ruleId, data]) => ({ ruleId, count: data.count, worstLevel: data.worstLevel }))
|
|
388
|
-
.sort((a, b) => b.count - a.count);
|
|
389
|
-
|
|
390
|
-
const blockers = entries.filter(e => e.worstLevel === 'CRITICAL' || e.worstLevel === 'HIGH');
|
|
391
|
-
const nonBlockers = entries.filter(e => e.worstLevel !== 'CRITICAL' && e.worstLevel !== 'HIGH');
|
|
392
|
-
|
|
393
|
-
blockers.forEach(({ ruleId, count, worstLevel }) => {
|
|
394
|
-
console.error(`${emojiForLevel(worstLevel)} ${ruleId} - ${count} violations`);
|
|
392
|
+
grouped[f.ruleId] = (grouped[f.ruleId] || 0) + 1;
|
|
395
393
|
});
|
|
396
394
|
|
|
397
|
-
|
|
398
|
-
.
|
|
399
|
-
.
|
|
400
|
-
|
|
395
|
+
Object.entries(grouped)
|
|
396
|
+
.sort((a, b) => b[1] - a[1])
|
|
397
|
+
.slice(0, 20)
|
|
398
|
+
.forEach(([ruleId, count]) => {
|
|
399
|
+
const severity = ruleId.includes("types.any") || ruleId.includes("security.") || ruleId.includes("architecture.") ? "error" :
|
|
400
|
+
ruleId.includes("performance.") || ruleId.includes("debug.") ? "warning" : "info";
|
|
401
|
+
const emoji = severity === "error" ? "🔴" : severity === "warning" ? "🟡" : "🔵";
|
|
402
|
+
console.error(`${emoji} ${ruleId} - ${count} violations`);
|
|
401
403
|
});
|
|
402
404
|
|
|
403
405
|
// Summary
|
|
@@ -474,6 +476,8 @@ function saveDetailedReport(findings, levelTotals, platformTotals, project, root
|
|
|
474
476
|
totalFiles: project.getSourceFiles().length,
|
|
475
477
|
timestamp: new Date().toISOString(),
|
|
476
478
|
root,
|
|
479
|
+
stagingOnlyMode: !!(context && context.stagingOnlyMode),
|
|
480
|
+
stagedFiles: Array.isArray(context && context.stagedFiles) ? context.stagedFiles : [],
|
|
477
481
|
},
|
|
478
482
|
};
|
|
479
483
|
|
|
@@ -593,7 +597,7 @@ function listSourceFiles(root) {
|
|
|
593
597
|
})
|
|
594
598
|
.filter(f => fs.existsSync(f) && !shouldIgnore(f.replace(/\\/g, "/")));
|
|
595
599
|
|
|
596
|
-
//
|
|
600
|
+
// If there are staged files but none are compatible with AST
|
|
597
601
|
if (stagedFiles.length === 0 && allStaged.length > 0) {
|
|
598
602
|
console.error('\n⚠️ No AST-compatible files in staging area');
|
|
599
603
|
console.error(' Staged files found:', allStaged.length);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
const { execSync } = require('child_process');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const env = require('../../../config/env');
|
|
5
7
|
|
|
6
8
|
class iOSASTIntelligentAnalyzer {
|
|
7
9
|
constructor(findings) {
|
|
@@ -22,6 +24,30 @@ class iOSASTIntelligentAnalyzer {
|
|
|
22
24
|
this.closures = [];
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
resolveAuditTmpDir(repoRoot) {
|
|
28
|
+
const configured = String(env.get('AUDIT_TMP', '') || '').trim();
|
|
29
|
+
if (configured.length > 0) {
|
|
30
|
+
return path.isAbsolute(configured) ? configured : path.join(repoRoot, configured);
|
|
31
|
+
}
|
|
32
|
+
return path.join(repoRoot, '.audit_tmp');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
safeTempFilePath(repoRoot, displayPath) {
|
|
36
|
+
const tmpDir = this.resolveAuditTmpDir(repoRoot);
|
|
37
|
+
const hash = crypto.createHash('sha1').update(String(displayPath)).digest('hex').slice(0, 10);
|
|
38
|
+
const base = path.basename(displayPath, '.swift');
|
|
39
|
+
const filename = `${base}.${hash}.staged.swift`;
|
|
40
|
+
return path.join(tmpDir, filename);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
readStagedFileContent(repoRoot, relPath) {
|
|
44
|
+
try {
|
|
45
|
+
return execSync(`git show :"${relPath}"`, { encoding: 'utf8', cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
25
51
|
checkSourceKitten() {
|
|
26
52
|
try {
|
|
27
53
|
execSync(`${this.sourceKittenPath} version`, { stdio: 'pipe' });
|
|
@@ -50,7 +76,7 @@ class iOSASTIntelligentAnalyzer {
|
|
|
50
76
|
return false;
|
|
51
77
|
}
|
|
52
78
|
|
|
53
|
-
analyzeWithSwiftSyntax(filePath) {
|
|
79
|
+
analyzeWithSwiftSyntax(filePath, displayPath = null) {
|
|
54
80
|
if (!this.swiftSyntaxPath) return;
|
|
55
81
|
try {
|
|
56
82
|
const result = execSync(`"${this.swiftSyntaxPath}" "${filePath}"`, {
|
|
@@ -58,8 +84,9 @@ class iOSASTIntelligentAnalyzer {
|
|
|
58
84
|
});
|
|
59
85
|
const violations = JSON.parse(result);
|
|
60
86
|
for (const v of violations) {
|
|
87
|
+
const reportedPath = displayPath || filePath;
|
|
61
88
|
this.findings.push({
|
|
62
|
-
ruleId: v.ruleId, severity: v.severity, filePath,
|
|
89
|
+
ruleId: v.ruleId, severity: v.severity, filePath: reportedPath,
|
|
63
90
|
line: v.line, column: v.column, message: v.message
|
|
64
91
|
});
|
|
65
92
|
}
|
|
@@ -81,21 +108,46 @@ class iOSASTIntelligentAnalyzer {
|
|
|
81
108
|
}
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
analyzeFile(filePath) {
|
|
85
|
-
if (!filePath.endsWith('.swift')) return;
|
|
111
|
+
analyzeFile(filePath, options = {}) {
|
|
112
|
+
if (!filePath || !String(filePath).endsWith('.swift')) return;
|
|
113
|
+
|
|
114
|
+
const repoRoot = options.repoRoot || require('../ast-core').getRepoRoot();
|
|
115
|
+
const displayPath = options.displayPath || filePath;
|
|
116
|
+
const stagedRelPath = options.stagedRelPath || null;
|
|
117
|
+
const stagingOnly = env.get('STAGING_ONLY_MODE', '0') === '1';
|
|
118
|
+
|
|
119
|
+
let parsePath = filePath;
|
|
120
|
+
let contentOverride = null;
|
|
121
|
+
|
|
122
|
+
if (stagingOnly && stagedRelPath) {
|
|
123
|
+
const stagedContent = this.readStagedFileContent(repoRoot, stagedRelPath);
|
|
124
|
+
if (typeof stagedContent === 'string') {
|
|
125
|
+
const tmpPath = this.safeTempFilePath(repoRoot, stagedRelPath);
|
|
126
|
+
try {
|
|
127
|
+
fs.mkdirSync(path.dirname(tmpPath), { recursive: true });
|
|
128
|
+
fs.writeFileSync(tmpPath, stagedContent, 'utf8');
|
|
129
|
+
parsePath = tmpPath;
|
|
130
|
+
contentOverride = stagedContent;
|
|
131
|
+
} catch {
|
|
132
|
+
// Fall back to working tree file
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
86
136
|
|
|
87
137
|
if (this.hasSwiftSyntax) {
|
|
88
|
-
this.analyzeWithSwiftSyntax(
|
|
138
|
+
this.analyzeWithSwiftSyntax(parsePath, displayPath);
|
|
89
139
|
}
|
|
90
140
|
|
|
91
|
-
const ast = this.parseFile(
|
|
141
|
+
const ast = this.parseFile(parsePath);
|
|
92
142
|
if (!ast) return;
|
|
93
143
|
|
|
94
|
-
this.currentFilePath =
|
|
144
|
+
this.currentFilePath = displayPath;
|
|
95
145
|
this.resetCollections();
|
|
96
146
|
|
|
97
147
|
try {
|
|
98
|
-
this.fileContent =
|
|
148
|
+
this.fileContent = typeof contentOverride === 'string'
|
|
149
|
+
? contentOverride
|
|
150
|
+
: fs.readFileSync(parsePath, 'utf8');
|
|
99
151
|
} catch {
|
|
100
152
|
this.fileContent = '';
|
|
101
153
|
}
|
|
@@ -103,7 +155,7 @@ class iOSASTIntelligentAnalyzer {
|
|
|
103
155
|
const substructure = ast['key.substructure'] || [];
|
|
104
156
|
|
|
105
157
|
this.collectAllNodes(substructure, null);
|
|
106
|
-
this.analyzeCollectedNodes(
|
|
158
|
+
this.analyzeCollectedNodes(displayPath);
|
|
107
159
|
}
|
|
108
160
|
|
|
109
161
|
resetCollections() {
|
|
@@ -427,10 +479,28 @@ class iOSASTIntelligentAnalyzer {
|
|
|
427
479
|
}
|
|
428
480
|
}
|
|
429
481
|
|
|
482
|
+
countLinesInBody(node) {
|
|
483
|
+
const offset = Number(node['key.bodyoffset']);
|
|
484
|
+
const length = Number(node['key.bodylength']);
|
|
485
|
+
if (!Number.isFinite(offset) || !Number.isFinite(length) || offset < 0 || length <= 0) {
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const buf = Buffer.from(this.fileContent || '', 'utf8');
|
|
491
|
+
const slice = buf.subarray(offset, Math.min(buf.length, offset + length));
|
|
492
|
+
const text = slice.toString('utf8');
|
|
493
|
+
if (!text) return 0;
|
|
494
|
+
return text.split('\n').length;
|
|
495
|
+
} catch {
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
430
500
|
analyzeFunctionAST(node, filePath) {
|
|
431
501
|
const name = node['key.name'] || '';
|
|
432
502
|
const line = node['key.line'] || 1;
|
|
433
|
-
const bodyLength = node
|
|
503
|
+
const bodyLength = this.countLinesInBody(node) || 0;
|
|
434
504
|
const attributes = this.getAttributes(node);
|
|
435
505
|
const substructure = node['key.substructure'] || [];
|
|
436
506
|
|