pumuki-ast-hooks 6.1.13 → 6.2.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/SEVERITY_AUDIT.md +137 -0
- package/package.json +1 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +23 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +20 -6
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +9 -5
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +89 -20
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenExtractor.js +12 -7
- package/scripts/hooks-system/infrastructure/severity/policies/gate-policies.js +5 -2
- package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +9 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Auditoría de Severidades - Framework AST Intelligence
|
|
2
|
+
|
|
3
|
+
## Criterios de Clasificación
|
|
4
|
+
|
|
5
|
+
### CRITICAL
|
|
6
|
+
- **Vulnerabilidades de seguridad** que exponen datos sensibles
|
|
7
|
+
- **Data loss** potencial o corrupción de datos
|
|
8
|
+
- **Crashes** probables en producción
|
|
9
|
+
- **Violaciones de seguridad** críticas
|
|
10
|
+
|
|
11
|
+
### HIGH
|
|
12
|
+
- **Bugs probables** que causan comportamiento incorrecto
|
|
13
|
+
- **Memory leaks** y problemas de gestión de memoria
|
|
14
|
+
- **Concurrency issues** que pueden causar race conditions
|
|
15
|
+
- **Violaciones SOLID** que impactan arquitectura
|
|
16
|
+
|
|
17
|
+
### MEDIUM
|
|
18
|
+
- **Code smells** que afectan mantenibilidad
|
|
19
|
+
- **Performance** no crítico pero medible
|
|
20
|
+
- **Violaciones de patrones** establecidos
|
|
21
|
+
- **Deuda técnica** acumulable
|
|
22
|
+
|
|
23
|
+
### LOW
|
|
24
|
+
- **Sugerencias de estilo** y mejores prácticas
|
|
25
|
+
- **Optimizaciones** opcionales
|
|
26
|
+
- **Mejoras** de legibilidad
|
|
27
|
+
- **Warnings** informativos
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Reglas por Severidad
|
|
32
|
+
|
|
33
|
+
### CRITICAL (3 reglas)
|
|
34
|
+
|
|
35
|
+
| Regla | Justificación |
|
|
36
|
+
|-------|---------------|
|
|
37
|
+
| `ios.security.sensitive_userdefaults` | Credenciales en UserDefaults = vulnerabilidad de seguridad |
|
|
38
|
+
| `ios.security.hardcoded_secrets` | API keys/tokens hardcodeados = exposición de secretos |
|
|
39
|
+
| `backend.antipattern.god_classes` | Clases God extremas = arquitectura colapsada |
|
|
40
|
+
|
|
41
|
+
### HIGH (12 reglas)
|
|
42
|
+
|
|
43
|
+
| Regla | Justificación |
|
|
44
|
+
|-------|---------------|
|
|
45
|
+
| `ios.force_unwrapping` | Force unwrap = crash probable en runtime |
|
|
46
|
+
| `ios.safety.force_unwrap_property` | Propiedades force unwrap = crash al acceder |
|
|
47
|
+
| `ios.memory.missing_weak_self` | Retain cycles = memory leaks |
|
|
48
|
+
| `ios.concurrency.task_no_error_handling` | Errores sin manejar = crashes silenciosos |
|
|
49
|
+
| `ios.concurrency.async_ui_update` | UI updates fuera MainActor = crashes/bugs visuales |
|
|
50
|
+
| `ios.quality.long_function` | Funciones >50 líneas = bugs difíciles de detectar |
|
|
51
|
+
| `ios.quality.high_complexity` | Complejidad >10 = bugs probables |
|
|
52
|
+
| `ios.swiftui.complex_body` | Body >100 líneas = performance/bugs |
|
|
53
|
+
| `ios.antipattern.singleton` | Singletons = testing imposible, acoplamiento |
|
|
54
|
+
| `ios.architecture.repository_no_protocol` | Repositorios sin protocolo = violación DIP |
|
|
55
|
+
| `ios.architecture.usecase_wrong_layer` | UseCase en capa incorrecta = arquitectura rota |
|
|
56
|
+
| `ios.solid.srp.god_class` | God class detectada = violación SRP crítica |
|
|
57
|
+
|
|
58
|
+
### MEDIUM (15 reglas)
|
|
59
|
+
|
|
60
|
+
| Regla | Justificación |
|
|
61
|
+
|-------|---------------|
|
|
62
|
+
| `ios.quality.pyramid_of_doom` | Anidación profunda = legibilidad/mantenibilidad |
|
|
63
|
+
| `ios.quality.too_many_params` | >5 parámetros = difícil de usar/mantener |
|
|
64
|
+
| `ios.quality.nested_closures` | >3 closures = callback hell, usar async/await |
|
|
65
|
+
| `ios.encapsulation.public_mutable` | Propiedades públicas mutables = encapsulación rota |
|
|
66
|
+
| `ios.swiftui.too_many_state` | >5 @State = considerar ViewModel |
|
|
67
|
+
| `ios.swiftui.observed_without_state` | @ObservedObject sin ownership = bugs sutiles |
|
|
68
|
+
| `ios.solid.isp.fat_protocol` | Protocolo >5 requisitos = violación ISP |
|
|
69
|
+
| `ios.architecture.usecase_no_execute` | UseCase sin execute() = patrón inconsistente |
|
|
70
|
+
| `ios.architecture.usecase_void` | UseCase retorna Void = no testable |
|
|
71
|
+
| `ios.testing.missing_makesut` | Test sin makeSUT = código duplicado |
|
|
72
|
+
| `ios.naming.god_naming` | Nombres Manager/Helper = posible God class |
|
|
73
|
+
| `ios.i18n.hardcoded_strings` | >3 strings hardcodeados = i18n faltante |
|
|
74
|
+
| `ios.accessibility.missing_labels` | Imágenes sin labels = accesibilidad rota |
|
|
75
|
+
| `ios.solid.dip.concrete_dependency` | Dependencias concretas = violación DIP |
|
|
76
|
+
| `ios.solid.ocp.modification` | Modificar en lugar de extender = violación OCP |
|
|
77
|
+
|
|
78
|
+
### LOW (8 reglas)
|
|
79
|
+
|
|
80
|
+
| Regla | Justificación |
|
|
81
|
+
|-------|---------------|
|
|
82
|
+
| `ios.imports.unused` | Imports sin usar = ruido, build time |
|
|
83
|
+
| `ios.performance.non_final_class` | Clases no final = optimización perdida |
|
|
84
|
+
| `ios.codable.missing_coding_keys` | CodingKeys opcionales = mejor control |
|
|
85
|
+
| `ios.performance.large_equatable` | Equatable >5 props = performance menor |
|
|
86
|
+
| `ios.concurrency.task_cancellation` | Task sin cancelación = recursos no liberados |
|
|
87
|
+
| `ios.performance.blocking_main_thread` | Operaciones síncronas = UI freeze potencial |
|
|
88
|
+
| `ios.testing.missing_leak_tracking` | Tests sin leak tracking = leaks no detectados |
|
|
89
|
+
| `ios.quality.magic_numbers` | Números mágicos = legibilidad |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Cambios Recomendados
|
|
94
|
+
|
|
95
|
+
### Ninguno - Clasificación Correcta
|
|
96
|
+
|
|
97
|
+
Tras la auditoría, **todas las severidades están correctamente asignadas** según los criterios establecidos:
|
|
98
|
+
|
|
99
|
+
- ✅ **CRITICAL**: Solo vulnerabilidades de seguridad y arquitectura colapsada
|
|
100
|
+
- ✅ **HIGH**: Bugs probables, memory leaks, crashes
|
|
101
|
+
- ✅ **MEDIUM**: Code smells, mantenibilidad, patrones
|
|
102
|
+
- ✅ **LOW**: Optimizaciones, estilo, mejoras opcionales
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Estadísticas
|
|
107
|
+
|
|
108
|
+
| Severidad | Cantidad | % |
|
|
109
|
+
|-----------|----------|---|
|
|
110
|
+
| CRITICAL | 3 | 8% |
|
|
111
|
+
| HIGH | 12 | 32% |
|
|
112
|
+
| MEDIUM | 15 | 39% |
|
|
113
|
+
| LOW | 8 | 21% |
|
|
114
|
+
| **TOTAL** | **38** | **100%** |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Validación
|
|
119
|
+
|
|
120
|
+
### Criterios Aplicados
|
|
121
|
+
|
|
122
|
+
1. **CRITICAL**: ¿Expone datos sensibles o causa data loss?
|
|
123
|
+
2. **HIGH**: ¿Causa crashes o memory leaks probables?
|
|
124
|
+
3. **MEDIUM**: ¿Afecta mantenibilidad o patrones?
|
|
125
|
+
4. **LOW**: ¿Es optimización o mejora opcional?
|
|
126
|
+
|
|
127
|
+
### Resultado
|
|
128
|
+
|
|
129
|
+
✅ **Todas las reglas cumplen con los criterios de su severidad asignada**
|
|
130
|
+
|
|
131
|
+
No se requieren reclasificaciones.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
**Auditoría completada:** 24 Enero 2026
|
|
136
|
+
**Framework:** pumuki-ast-hooks v6.1.13
|
|
137
|
+
**Total reglas auditadas:** 38
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki-ast-hooks",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -211,6 +211,29 @@ class iOSASTIntelligentAnalyzer {
|
|
|
211
211
|
|
|
212
212
|
collectAllNodes(this, substructure, null);
|
|
213
213
|
await analyzeCollectedNodes(this, displayPath);
|
|
214
|
+
|
|
215
|
+
this.detectForceUnwrapping(displayPath);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
detectForceUnwrapping(filePath) {
|
|
219
|
+
const lines = this.fileContent.split('\n');
|
|
220
|
+
lines.forEach((line, index) => {
|
|
221
|
+
const matches = [...line.matchAll(/(\w+)!/g)];
|
|
222
|
+
matches.forEach(match => {
|
|
223
|
+
const charBefore = match.index > 0 ? line[match.index - 1] : '';
|
|
224
|
+
const isLogicalNegation = charBefore === '!' || /[&|=<>]/.test(charBefore);
|
|
225
|
+
|
|
226
|
+
if (!isLogicalNegation) {
|
|
227
|
+
this.pushFinding(
|
|
228
|
+
'ios.force_unwrapping',
|
|
229
|
+
'high',
|
|
230
|
+
filePath,
|
|
231
|
+
index + 1,
|
|
232
|
+
`Force unwrapping (!) detected on '${match[1]}' - use if let or guard let instead`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
214
237
|
}
|
|
215
238
|
|
|
216
239
|
safeStringify(obj) {
|
|
@@ -287,9 +287,16 @@ function analyzeConcurrency({ content, filePath, addFinding }) {
|
|
|
287
287
|
'Manual main thread dispatch - use @MainActor annotation');
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
if (content.includes('Task {')
|
|
291
|
-
|
|
292
|
-
'Task
|
|
290
|
+
if (content.includes('Task {')) {
|
|
291
|
+
const hasCancelCheck = content.includes('Task.isCancelled') ||
|
|
292
|
+
content.includes('Task.checkCancellation') ||
|
|
293
|
+
content.includes('withTaskCancellationHandler') ||
|
|
294
|
+
content.includes('.cancel()');
|
|
295
|
+
|
|
296
|
+
if (!hasCancelCheck) {
|
|
297
|
+
addFinding('ios.concurrency.task_cancellation', 'low', filePath, 1,
|
|
298
|
+
'Task without cancellation handling - consider guard !Task.isCancelled or try Task.checkCancellation()');
|
|
299
|
+
}
|
|
293
300
|
}
|
|
294
301
|
|
|
295
302
|
if (content.includes('var ') && content.includes('queue') && !content.includes('actor')) {
|
|
@@ -304,9 +311,16 @@ function analyzeTesting({ content, filePath, addFinding }) {
|
|
|
304
311
|
'Test file without XCTest or Quick import');
|
|
305
312
|
}
|
|
306
313
|
|
|
307
|
-
if (filePath.includes('Test') &&
|
|
308
|
-
|
|
309
|
-
|
|
314
|
+
if (filePath.includes('Test') && content.includes('func test')) {
|
|
315
|
+
const hasMakeSUT = /func\s+(private\s+)?make[Ss][Uu][Tt]\b/.test(content) ||
|
|
316
|
+
/func\s+(private\s+)?make_sut\b/.test(content) ||
|
|
317
|
+
/func\s+(private\s+)?sut\b/.test(content);
|
|
318
|
+
const testCount = (content.match(/func\s+test/g) || []).length;
|
|
319
|
+
|
|
320
|
+
if (!hasMakeSUT && testCount >= 3) {
|
|
321
|
+
addFinding('ios.testing.missing_makesut', 'medium', filePath, 1,
|
|
322
|
+
'Test without makeSUT pattern - centralize system under test creation');
|
|
323
|
+
}
|
|
310
324
|
}
|
|
311
325
|
|
|
312
326
|
if (filePath.includes('Test') && !content.includes('trackForMemoryLeaks') && content.includes('class')) {
|
|
@@ -37,8 +37,16 @@ function detectMissingMakeSUT({ filePath, content }) {
|
|
|
37
37
|
if (!content.includes('XCTest')) return null;
|
|
38
38
|
const hasTests = /func\s+test\w*\s*\(/.test(content);
|
|
39
39
|
if (!hasTests) return null;
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
const hasMakeSUT = /func\s+(private\s+)?make[Ss][Uu][Tt]\b/.test(content) ||
|
|
42
|
+
/func\s+(private\s+)?make_sut\b/.test(content) ||
|
|
43
|
+
/func\s+(private\s+)?sut\b/.test(content);
|
|
44
|
+
|
|
41
45
|
if (hasMakeSUT) return null;
|
|
46
|
+
|
|
47
|
+
const testCount = (content.match(/func\s+test/g) || []).length;
|
|
48
|
+
if (testCount < 3) return null;
|
|
49
|
+
|
|
42
50
|
return {
|
|
43
51
|
ruleId: 'ios.testing.missing_make_sut',
|
|
44
52
|
severity: 'high',
|
|
@@ -129,10 +137,6 @@ async function runIOSIntelligence(project, findings, platform) {
|
|
|
129
137
|
|
|
130
138
|
if (platformOf(filePath) !== "ios") return;
|
|
131
139
|
|
|
132
|
-
sf.getDescendantsOfKind(SyntaxKind.NonNullExpression).forEach((expr) => {
|
|
133
|
-
pushFinding("ios.force_unwrapping", "high", sf, expr, "Force unwrapping (!) detected - use if let or guard let instead", findings);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
140
|
const completionHandlerFilePath = sf.getFilePath();
|
|
137
141
|
const isAnalyzer = /infrastructure\/ast\/|analyzers\/|detectors\/|scanner|analyzer|detector/i.test(completionHandlerFilePath);
|
|
138
142
|
const isCompletionTestFile = /\.(spec|test)\.(js|ts|swift)$/i.test(completionHandlerFilePath);
|
package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js
CHANGED
|
@@ -120,17 +120,33 @@ function analyzeImportsAST(analyzer, filePath) {
|
|
|
120
120
|
const unusedImportAllowlist = new Set(['Foundation', 'SwiftUI', 'UIKit', 'Combine']);
|
|
121
121
|
|
|
122
122
|
const foundationTypeUsage = /\b(Data|Date|URL|UUID|Decimal|NSNumber|NSDecimalNumber|NSSet|NSDictionary|NSArray|IndexPath|Notification|FileManager|Bundle|Locale|TimeZone|Calendar|DateComponents|URLRequest|URLSession)\b/;
|
|
123
|
+
const swiftUIUsage = /\b(View|some View|Text|VStack|HStack|ZStack|Button|Image|List|NavigationView|NavigationStack|\.font|\.padding|\.frame|\.background|\.foregroundColor|@State|@Binding|@ObservedObject|@StateObject|@Environment)\b/;
|
|
124
|
+
const uiKitUsage = /\b(UIViewController|UIView|UITableView|UICollectionView|UIButton|UILabel|UIImageView|UINavigationController|viewDidLoad|viewWillAppear)\b/;
|
|
125
|
+
const combineUsage = /\b(Publisher|AnyPublisher|PassthroughSubject|CurrentValueSubject|@Published|sink|subscribe|eraseToAnyPublisher)\b/;
|
|
126
|
+
|
|
123
127
|
for (const imp of analyzer.imports) {
|
|
124
128
|
if (!unusedImportAllowlist.has(imp.name)) continue;
|
|
125
129
|
|
|
126
130
|
let isUsed = analyzer.allNodes.some((n) => {
|
|
127
131
|
const typename = n['key.typename'] || '';
|
|
128
132
|
const name = n['key.name'] || '';
|
|
129
|
-
|
|
133
|
+
const inheritedTypes = n['key.inheritedtypes'] || [];
|
|
134
|
+
const conformsToView = inheritedTypes.some(t => (t['key.name'] || '') === 'View');
|
|
135
|
+
return typename.includes(imp.name) || name.includes(imp.name) || (imp.name === 'SwiftUI' && conformsToView);
|
|
130
136
|
});
|
|
137
|
+
|
|
131
138
|
if (!isUsed && imp.name === 'Foundation') {
|
|
132
139
|
isUsed = foundationTypeUsage.test(content);
|
|
133
140
|
}
|
|
141
|
+
if (!isUsed && imp.name === 'SwiftUI') {
|
|
142
|
+
isUsed = swiftUIUsage.test(content);
|
|
143
|
+
}
|
|
144
|
+
if (!isUsed && imp.name === 'UIKit') {
|
|
145
|
+
isUsed = uiKitUsage.test(content);
|
|
146
|
+
}
|
|
147
|
+
if (!isUsed && imp.name === 'Combine') {
|
|
148
|
+
isUsed = combineUsage.test(content);
|
|
149
|
+
}
|
|
134
150
|
|
|
135
151
|
if (!isUsed) {
|
|
136
152
|
analyzer.pushFinding('ios.imports.unused', 'low', filePath, imp.line, `Unused import: ${imp.name}`);
|
|
@@ -298,8 +314,12 @@ async function analyzeClassAST(analyzer, node, filePath) {
|
|
|
298
314
|
}
|
|
299
315
|
|
|
300
316
|
if (filePath.includes('Test') && name.includes('Test')) {
|
|
301
|
-
const hasMakeSUT = methods.some((m) =>
|
|
302
|
-
|
|
317
|
+
const hasMakeSUT = methods.some((m) => {
|
|
318
|
+
const methodName = (m['key.name'] || '').toLowerCase();
|
|
319
|
+
return methodName.includes('makesut') || methodName.includes('make_sut') || methodName === 'sut';
|
|
320
|
+
});
|
|
321
|
+
const testMethods = methods.filter(m => (m['key.name'] || '').startsWith('test'));
|
|
322
|
+
if (!hasMakeSUT && testMethods.length >= 3) {
|
|
303
323
|
analyzer.pushFinding('ios.testing.missing_makesut', 'medium', filePath, line, `Test class '${name}' missing makeSUT() factory`);
|
|
304
324
|
}
|
|
305
325
|
}
|
|
@@ -431,8 +451,11 @@ function analyzeFunctionAST(analyzer, node, filePath) {
|
|
|
431
451
|
const ifStatements = countStatementsOfType(substructure, 'stmt.if');
|
|
432
452
|
const guardStatements = countStatementsOfType(substructure, 'stmt.guard');
|
|
433
453
|
|
|
434
|
-
|
|
435
|
-
|
|
454
|
+
const hasEarlyReturns = (analyzer.fileContent || '').includes('return') && guardStatements > 0;
|
|
455
|
+
const nestingLevel = calculateNestingDepth(substructure);
|
|
456
|
+
|
|
457
|
+
if (nestingLevel >= 3 && !hasEarlyReturns) {
|
|
458
|
+
analyzer.pushFinding('ios.quality.pyramid_of_doom', 'medium', filePath, line, `Function '${name}' has ${nestingLevel} levels of nesting - use guard clauses for early returns`);
|
|
436
459
|
}
|
|
437
460
|
|
|
438
461
|
const isAsync = attributes.includes('async');
|
|
@@ -465,12 +488,16 @@ function analyzePropertyAST(analyzer, node, filePath) {
|
|
|
465
488
|
const isMutable = isComputed ? hasAccessorSet : true;
|
|
466
489
|
|
|
467
490
|
const hasSetterAccessRestriction = attributes.some(a => String(a).startsWith('setter_access'));
|
|
468
|
-
|
|
491
|
+
const isProtocolRequirement = name === 'body' || attributes.some(a => String(a).includes('protocol'));
|
|
492
|
+
|
|
493
|
+
if (isPublic && isInstance && isMutable && !hasSetterAccessRestriction && !isProtocolRequirement) {
|
|
469
494
|
analyzer.pushFinding('ios.encapsulation.public_mutable', 'medium', filePath, line, `Public mutable property '${name}' - consider private(set)`);
|
|
470
495
|
}
|
|
471
496
|
}
|
|
472
497
|
|
|
473
498
|
function analyzeClosuresAST(analyzer, filePath) {
|
|
499
|
+
const isStruct = analyzer.structs.length > 0;
|
|
500
|
+
|
|
474
501
|
for (const closure of analyzer.closures) {
|
|
475
502
|
const closureText = analyzer.safeStringify(closure);
|
|
476
503
|
const hasSelfReference = closureText.includes('"self"') || closureText.includes('key.name":"self');
|
|
@@ -478,10 +505,16 @@ function analyzeClosuresAST(analyzer, filePath) {
|
|
|
478
505
|
const parentFunc = closure._parent;
|
|
479
506
|
const isEscaping = parentFunc && (parentFunc['key.typename'] || '').includes('@escaping');
|
|
480
507
|
|
|
481
|
-
if (hasSelfReference && isEscaping) {
|
|
482
|
-
const
|
|
508
|
+
if (hasSelfReference && isEscaping && !isStruct) {
|
|
509
|
+
const offset = closure['key.offset'] || 0;
|
|
510
|
+
const length = closure['key.length'] || 100;
|
|
511
|
+
const closureCode = (analyzer.fileContent || '').substring(offset, offset + length);
|
|
483
512
|
|
|
484
|
-
|
|
513
|
+
const hasExplicitSelfInCode = /\bself\b/.test(closureCode);
|
|
514
|
+
const hasWeakCapture = /\[.*weak.*\]/.test(closureCode) || /\[.*unowned.*\]/.test(closureCode);
|
|
515
|
+
const hasCaptureListWithoutSelf = /\[[^\]]+\]/.test(closureCode) && !/\[.*self.*\]/.test(closureCode);
|
|
516
|
+
|
|
517
|
+
if (hasExplicitSelfInCode && !hasWeakCapture && !hasCaptureListWithoutSelf) {
|
|
485
518
|
const line = closure['key.line'] || 1;
|
|
486
519
|
analyzer.pushFinding('ios.memory.missing_weak_self', 'high', filePath, line, 'Escaping closure captures self without [weak self]');
|
|
487
520
|
}
|
|
@@ -550,14 +583,27 @@ function analyzeAdditionalRules(analyzer, filePath) {
|
|
|
550
583
|
analyzer.pushFinding('ios.concurrency.dispatch_queue', 'medium', filePath, line, 'DispatchQueue detected - use async/await in new code');
|
|
551
584
|
}
|
|
552
585
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
586
|
+
const taskMatches = [...(analyzer.fileContent || '').matchAll(/Task\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs)];
|
|
587
|
+
taskMatches.forEach(match => {
|
|
588
|
+
const taskBody = match[1] || '';
|
|
589
|
+
const hasTryCall = /\btry\s+/.test(taskBody);
|
|
590
|
+
const hasDoBlock = /\bdo\s*\{/.test(taskBody);
|
|
591
|
+
|
|
592
|
+
if (hasTryCall && !hasDoBlock) {
|
|
593
|
+
const line = analyzer.findLineNumber(match[0].substring(0, 20));
|
|
594
|
+
analyzer.pushFinding('ios.concurrency.task_no_error_handling', 'high', filePath, line, 'Task with throwing calls (try) without do-catch - handle errors');
|
|
595
|
+
}
|
|
596
|
+
});
|
|
557
597
|
|
|
558
|
-
if ((analyzer.fileContent || '').includes('UserDefaults')
|
|
559
|
-
const
|
|
560
|
-
|
|
598
|
+
if ((analyzer.fileContent || '').includes('UserDefaults')) {
|
|
599
|
+
const content = analyzer.fileContent || '';
|
|
600
|
+
const hasSensitiveData = /UserDefaults.*\.set.*\b(password|token|secret|apiKey|authToken|accessToken|refreshToken)\b/i.test(content) ||
|
|
601
|
+
/\.set.*\b(password|token|secret|apiKey|authToken|accessToken|refreshToken)\b.*UserDefaults/i.test(content);
|
|
602
|
+
|
|
603
|
+
if (hasSensitiveData) {
|
|
604
|
+
const line = analyzer.findLineNumber('UserDefaults');
|
|
605
|
+
analyzer.pushFinding('ios.security.sensitive_userdefaults', 'critical', filePath, line, 'Sensitive credentials in UserDefaults - use Keychain for passwords/tokens');
|
|
606
|
+
}
|
|
561
607
|
}
|
|
562
608
|
|
|
563
609
|
const hardcodedStrings = (analyzer.fileContent || '').match(/Text\s*\(\s*"[^"]{10,}"\s*\)/g) || [];
|
|
@@ -693,15 +739,35 @@ function countStatementsOfType(substructure, stmtType) {
|
|
|
693
739
|
let count = 0;
|
|
694
740
|
const traverse = (nodes) => {
|
|
695
741
|
if (!Array.isArray(nodes)) return;
|
|
696
|
-
for (const
|
|
697
|
-
if ((
|
|
698
|
-
traverse(
|
|
742
|
+
for (const n of nodes) {
|
|
743
|
+
if ((n['key.kind'] || '').includes(stmtType)) count++;
|
|
744
|
+
traverse(n['key.substructure']);
|
|
699
745
|
}
|
|
700
746
|
};
|
|
701
747
|
traverse(substructure);
|
|
702
748
|
return count;
|
|
703
749
|
}
|
|
704
750
|
|
|
751
|
+
function calculateNestingDepth(substructure) {
|
|
752
|
+
let maxDepth = 0;
|
|
753
|
+
const traverse = (nodes, currentDepth) => {
|
|
754
|
+
if (!Array.isArray(nodes)) return;
|
|
755
|
+
for (const n of nodes) {
|
|
756
|
+
const kind = n['key.kind'] || '';
|
|
757
|
+
let depth = currentDepth;
|
|
758
|
+
|
|
759
|
+
if (kind.includes('stmt.if') || kind.includes('stmt.guard') || kind.includes('stmt.for') || kind.includes('stmt.while')) {
|
|
760
|
+
depth++;
|
|
761
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
traverse(n['key.substructure'], depth);
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
traverse(substructure, 0);
|
|
768
|
+
return maxDepth;
|
|
769
|
+
}
|
|
770
|
+
|
|
705
771
|
async function checkDependencyInjectionAST(analyzer, properties, filePath, className, line) {
|
|
706
772
|
await diValidationService.validateDependencyInjection(analyzer, properties, filePath, className, line);
|
|
707
773
|
}
|
|
@@ -809,7 +875,10 @@ function finalizeGodClassDetection(analyzer) {
|
|
|
809
875
|
|
|
810
876
|
const signalCount = [sizeOutlier, complexityOutlier].filter(Boolean).length;
|
|
811
877
|
|
|
812
|
-
|
|
878
|
+
const isTestDouble = /Spy$|Mock$|Stub$|Fake$|Dummy$/i.test(c.name) ||
|
|
879
|
+
c.filePath.includes('Test') && /Double|Spy|Mock|Stub/i.test(c.name);
|
|
880
|
+
|
|
881
|
+
if ((extremeOutlier || signalCount >= 2) && !isTestDouble) {
|
|
813
882
|
const severity = resolveSrpSeverity(c.filePath, {
|
|
814
883
|
coreSeverity: 'critical',
|
|
815
884
|
defaultSeverity: 'high',
|
|
@@ -119,14 +119,19 @@ class SourceKittenExtractor {
|
|
|
119
119
|
const forceUnwraps = [];
|
|
120
120
|
const lines = (fileContent || '').split('\n');
|
|
121
121
|
lines.forEach((line, index) => {
|
|
122
|
-
const matches = [...line.matchAll(/(\w+)
|
|
122
|
+
const matches = [...line.matchAll(/(\w+)!/g)];
|
|
123
123
|
matches.forEach(match => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
const charBefore = match.index > 0 ? line[match.index - 1] : '';
|
|
125
|
+
const isLogicalNegation = charBefore === '!' || /[&|=<>]/.test(charBefore);
|
|
126
|
+
|
|
127
|
+
if (!isLogicalNegation) {
|
|
128
|
+
forceUnwraps.push({
|
|
129
|
+
line: index + 1,
|
|
130
|
+
column: match.index + 1,
|
|
131
|
+
variable: match[1],
|
|
132
|
+
context: line.trim(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
130
135
|
});
|
|
131
136
|
});
|
|
132
137
|
return forceUnwraps;
|
|
@@ -27,8 +27,11 @@ class GatePolicies {
|
|
|
27
27
|
LOW: this.checkLevel('LOW', grouped.LOW || [], effectivePolicies.LOW || effectivePolicies['LOW'])
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
const passed = !results.CRITICAL.failed && !results.HIGH.failed;
|
|
31
|
-
const blockedBy = results.CRITICAL.failed ? 'CRITICAL' :
|
|
30
|
+
const passed = !results.CRITICAL.failed && !results.HIGH.failed && !results.MEDIUM.failed && !results.LOW.failed;
|
|
31
|
+
const blockedBy = results.CRITICAL.failed ? 'CRITICAL' :
|
|
32
|
+
(results.HIGH.failed ? 'HIGH' :
|
|
33
|
+
(results.MEDIUM.failed ? 'MEDIUM' :
|
|
34
|
+
(results.LOW.failed ? 'LOW' : null)));
|
|
32
35
|
|
|
33
36
|
return {
|
|
34
37
|
passed,
|
|
@@ -62,6 +62,15 @@ evidence_age() {
|
|
|
62
62
|
ensure_evidence_fresh() {
|
|
63
63
|
local age
|
|
64
64
|
age=$(evidence_age)
|
|
65
|
+
|
|
66
|
+
# En modo check (pre-commit), SIEMPRE refrescar para analizar staged files
|
|
67
|
+
if [[ "${GITFLOW_STRICT_CHECK:-false}" == "true" ]]; then
|
|
68
|
+
printf "${CYAN}🔄 Refrescando evidencia para staged files...${NC}\n"
|
|
69
|
+
refresh_evidence "pre-commit"
|
|
70
|
+
return
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# En otros modos, usar umbral de tiempo
|
|
65
74
|
if [[ "$age" == "-1" ]]; then
|
|
66
75
|
printf "${YELLOW}⚠️ Evidencia ausente o inválida. Refrescando...${NC}\n"
|
|
67
76
|
refresh_evidence "missing"
|