pumuki 6.3.190 → 6.3.192
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/core/facts/detectors/text/ios.test.ts +26 -0
- package/core/facts/detectors/text/ios.ts +27 -0
- package/core/facts/extractHeuristicFacts.ts +2 -0
- package/core/rules/presets/heuristics/ios.test.ts +11 -1
- package/core/rules/presets/heuristics/ios.ts +36 -0
- package/docs/codex-skills/ios-enterprise-rules.md +5 -0
- package/integrations/config/skillsDetectorRegistry.ts +8 -0
- package/integrations/config/skillsRuleSet.ts +8 -4
- package/integrations/lifecycle/audit.ts +23 -2
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
hasSwiftDispatchGroupUsage,
|
|
13
13
|
hasSwiftDispatchQueueUsage,
|
|
14
14
|
hasSwiftDispatchSemaphoreUsage,
|
|
15
|
+
hasSwiftAdHocLoggingUsage,
|
|
15
16
|
hasSwiftForEachIndicesUsage,
|
|
16
17
|
hasSwiftForceCastUsage,
|
|
17
18
|
hasSwiftFontWeightBoldUsage,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
hasSwiftPreconcurrencyUsage,
|
|
42
43
|
hasSwiftSheetIsPresentedUsage,
|
|
43
44
|
hasSwiftScrollViewShowsIndicatorsUsage,
|
|
45
|
+
hasSwiftSensitiveLoggingUsage,
|
|
44
46
|
hasSwiftStringFormatUsage,
|
|
45
47
|
hasSwiftTabItemUsage,
|
|
46
48
|
hasSwiftTaskDetachedUsage,
|
|
@@ -165,6 +167,30 @@ Task {
|
|
|
165
167
|
assert.equal(hasSwiftTaskDetachedUsage(negative), false);
|
|
166
168
|
});
|
|
167
169
|
|
|
170
|
+
test('detectores de logging iOS detectan logs ad-hoc y PII en produccion', () => {
|
|
171
|
+
const adHoc = `
|
|
172
|
+
print(user.id)
|
|
173
|
+
debugPrint(response)
|
|
174
|
+
dump(model)
|
|
175
|
+
NSLog("legacy")
|
|
176
|
+
os_log("legacy")
|
|
177
|
+
`;
|
|
178
|
+
const structuredSafe = `
|
|
179
|
+
logger.info("Screen loaded")
|
|
180
|
+
let text = "print(accessToken)"
|
|
181
|
+
// print(accessToken)
|
|
182
|
+
`;
|
|
183
|
+
const sensitive = `
|
|
184
|
+
print(accessToken)
|
|
185
|
+
logger.error("Refresh failed \\(refreshToken)")
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
assert.equal(hasSwiftAdHocLoggingUsage(adHoc), true);
|
|
189
|
+
assert.equal(hasSwiftAdHocLoggingUsage(structuredSafe), false);
|
|
190
|
+
assert.equal(hasSwiftSensitiveLoggingUsage(sensitive), true);
|
|
191
|
+
assert.equal(hasSwiftSensitiveLoggingUsage(structuredSafe), false);
|
|
192
|
+
});
|
|
193
|
+
|
|
168
194
|
test('hasSwiftUncheckedSendableUsage detecta @unchecked Sendable', () => {
|
|
169
195
|
const source = `
|
|
170
196
|
final class LegacyBox: @unchecked Sendable {}
|
|
@@ -437,6 +437,33 @@ export const hasSwiftTaskDetachedUsage = (source: string): boolean => {
|
|
|
437
437
|
});
|
|
438
438
|
};
|
|
439
439
|
|
|
440
|
+
export const hasSwiftAdHocLoggingUsage = (source: string): boolean => {
|
|
441
|
+
return collectSwiftRegexLines(
|
|
442
|
+
source,
|
|
443
|
+
/\b(?:print|debugPrint|dump|NSLog|os_log)\s*\(/
|
|
444
|
+
).length > 0;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export const hasSwiftSensitiveLoggingUsage = (source: string): boolean => {
|
|
448
|
+
return source.split(/\r?\n/).some((line) => {
|
|
449
|
+
const sanitized = stripSwiftLineForSemanticScan(line);
|
|
450
|
+
const lineWithoutComments = line.replace(/\/\/.*$/, '');
|
|
451
|
+
const hasLoggingCall =
|
|
452
|
+
/\b(?:print|debugPrint|dump|NSLog|os_log)\s*\(/.test(sanitized) ||
|
|
453
|
+
/\b(?:logger|log)\s*\.\s*(?:debug|info|notice|warning|error|critical|log)\s*\(/i.test(
|
|
454
|
+
sanitized
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
if (!hasLoggingCall) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return /\b(?:accessToken|refreshToken|authToken|token|password|secret|credential|authorization|email|userId)\b/i.test(
|
|
462
|
+
lineWithoutComments
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
|
|
440
467
|
export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
|
|
441
468
|
return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
|
|
442
469
|
if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
|
|
@@ -618,6 +618,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
618
618
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDispatchSemaphoreUsage, ruleId: 'heuristics.ios.dispatchsemaphore.ast', code: 'HEURISTICS_IOS_DISPATCHSEMAPHORE_AST', message: 'AST heuristic detected DispatchSemaphore usage.' },
|
|
619
619
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOperationQueueUsage, ruleId: 'heuristics.ios.operation-queue.ast', code: 'HEURISTICS_IOS_OPERATION_QUEUE_AST', message: 'AST heuristic detected OperationQueue usage.' },
|
|
620
620
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftTaskDetachedUsage, ruleId: 'heuristics.ios.task-detached.ast', code: 'HEURISTICS_IOS_TASK_DETACHED_AST', message: 'AST heuristic detected Task.detached usage.' },
|
|
621
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAdHocLoggingUsage, ruleId: 'heuristics.ios.logging.adhoc-print.ast', code: 'HEURISTICS_IOS_LOGGING_ADHOC_PRINT_AST', message: 'AST heuristic detected print/debugPrint/dump/NSLog/os_log usage in iOS production code.' },
|
|
622
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftSensitiveLoggingUsage, ruleId: 'heuristics.ios.logging.sensitive-data.ast', code: 'HEURISTICS_IOS_LOGGING_SENSITIVE_DATA_AST', message: 'AST heuristic detected sensitive data in an iOS logging call.' },
|
|
621
623
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUncheckedSendableUsage, ruleId: 'heuristics.ios.unchecked-sendable.ast', code: 'HEURISTICS_IOS_UNCHECKED_SENDABLE_AST', message: 'AST heuristic detected @unchecked Sendable usage.' },
|
|
622
624
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPreconcurrencyUsage, ruleId: 'heuristics.ios.preconcurrency.ast', code: 'HEURISTICS_IOS_PRECONCURRENCY_AST', message: 'AST heuristic detected @preconcurrency usage.' },
|
|
623
625
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonisolatedUnsafeUsage, ruleId: 'heuristics.ios.nonisolated-unsafe.ast', code: 'HEURISTICS_IOS_NONISOLATED_UNSAFE_AST', message: 'AST heuristic detected nonisolated(unsafe) usage.' },
|
|
@@ -3,7 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import { iosRules } from './ios';
|
|
4
4
|
|
|
5
5
|
test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
6
|
-
assert.equal(iosRules.length,
|
|
6
|
+
assert.equal(iosRules.length, 45);
|
|
7
7
|
|
|
8
8
|
const ids = iosRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -17,6 +17,8 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
17
17
|
'heuristics.ios.dispatchsemaphore.ast',
|
|
18
18
|
'heuristics.ios.operation-queue.ast',
|
|
19
19
|
'heuristics.ios.task-detached.ast',
|
|
20
|
+
'heuristics.ios.logging.adhoc-print.ast',
|
|
21
|
+
'heuristics.ios.logging.sensitive-data.ast',
|
|
20
22
|
'heuristics.ios.unchecked-sendable.ast',
|
|
21
23
|
'heuristics.ios.preconcurrency.ast',
|
|
22
24
|
'heuristics.ios.nonisolated-unsafe.ast',
|
|
@@ -61,6 +63,14 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
61
63
|
byId.get('heuristics.ios.task-detached.ast')?.then.code,
|
|
62
64
|
'HEURISTICS_IOS_TASK_DETACHED_AST'
|
|
63
65
|
);
|
|
66
|
+
assert.equal(
|
|
67
|
+
byId.get('heuristics.ios.logging.adhoc-print.ast')?.then.code,
|
|
68
|
+
'HEURISTICS_IOS_LOGGING_ADHOC_PRINT_AST'
|
|
69
|
+
);
|
|
70
|
+
assert.equal(
|
|
71
|
+
byId.get('heuristics.ios.logging.sensitive-data.ast')?.then.code,
|
|
72
|
+
'HEURISTICS_IOS_LOGGING_SENSITIVE_DATA_AST'
|
|
73
|
+
);
|
|
64
74
|
assert.equal(
|
|
65
75
|
byId.get('heuristics.ios.preconcurrency.ast')?.then.code,
|
|
66
76
|
'HEURISTICS_IOS_PRECONCURRENCY_AST'
|
|
@@ -181,6 +181,42 @@ export const iosRules: RuleSet = [
|
|
|
181
181
|
code: 'HEURISTICS_IOS_TASK_DETACHED_AST',
|
|
182
182
|
},
|
|
183
183
|
},
|
|
184
|
+
{
|
|
185
|
+
id: 'heuristics.ios.logging.adhoc-print.ast',
|
|
186
|
+
description: 'Detects print/debugPrint/dump/NSLog/os_log usage in iOS production code.',
|
|
187
|
+
severity: 'WARN',
|
|
188
|
+
platform: 'ios',
|
|
189
|
+
locked: true,
|
|
190
|
+
when: {
|
|
191
|
+
kind: 'Heuristic',
|
|
192
|
+
where: {
|
|
193
|
+
ruleId: 'heuristics.ios.logging.adhoc-print.ast',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
then: {
|
|
197
|
+
kind: 'Finding',
|
|
198
|
+
message: 'AST heuristic detected print/debugPrint/dump/NSLog/os_log usage in iOS production code.',
|
|
199
|
+
code: 'HEURISTICS_IOS_LOGGING_ADHOC_PRINT_AST',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: 'heuristics.ios.logging.sensitive-data.ast',
|
|
204
|
+
description: 'Detects sensitive data in iOS logging calls.',
|
|
205
|
+
severity: 'WARN',
|
|
206
|
+
platform: 'ios',
|
|
207
|
+
locked: true,
|
|
208
|
+
when: {
|
|
209
|
+
kind: 'Heuristic',
|
|
210
|
+
where: {
|
|
211
|
+
ruleId: 'heuristics.ios.logging.sensitive-data.ast',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
then: {
|
|
215
|
+
kind: 'Finding',
|
|
216
|
+
message: 'AST heuristic detected sensitive data in an iOS logging call.',
|
|
217
|
+
code: 'HEURISTICS_IOS_LOGGING_SENSITIVE_DATA_AST',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
184
220
|
{
|
|
185
221
|
id: 'heuristics.ios.unchecked-sendable.ast',
|
|
186
222
|
description: 'Detects @unchecked Sendable usage in iOS production code.',
|
|
@@ -223,6 +223,11 @@ apps/ios/Presentation/
|
|
|
223
223
|
✅ **Prohibido print()** y logs ad-hoc
|
|
224
224
|
✅ **No loggear PII** (tokens, emails, IDs sensibles)
|
|
225
225
|
|
|
226
|
+
### Enforcement AST inicial de logging iOS:
|
|
227
|
+
✅ `skills.ios.guideline.ios.prohibido-print-y-logs-ad-hoc` se mapea a `heuristics.ios.logging.adhoc-print.ast` para detectar `print`, `debugPrint`, `dump`, `NSLog` y `os_log` en Swift production.
|
|
228
|
+
✅ `skills.ios.guideline.ios.no-loggear-pii-tokens-emails-ids-sensibles` se mapea a `heuristics.ios.logging.sensitive-data.ast` para detectar tokens, credenciales, emails e IDs sensibles en llamadas de logging.
|
|
229
|
+
✅ `os.Logger` sigue siendo la API preferida; esta slice detecta el riesgo prohibido, no fuerza todavía una arquitectura completa de observabilidad.
|
|
230
|
+
|
|
226
231
|
```swift
|
|
227
232
|
// ✅ Ejemplo: ViewModel con @Observable (iOS 17+)
|
|
228
233
|
@Observable
|
|
@@ -47,6 +47,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
47
47
|
'skills.ios.no-task-detached': heuristicDetector('ios.task-detached', [
|
|
48
48
|
'heuristics.ios.task-detached.ast',
|
|
49
49
|
]),
|
|
50
|
+
'skills.ios.guideline.ios.prohibido-print-y-logs-ad-hoc': heuristicDetector(
|
|
51
|
+
'ios.logging.adhoc-print',
|
|
52
|
+
['heuristics.ios.logging.adhoc-print.ast']
|
|
53
|
+
),
|
|
54
|
+
'skills.ios.guideline.ios.no-loggear-pii-tokens-emails-ids-sensibles': heuristicDetector(
|
|
55
|
+
'ios.logging.sensitive-data',
|
|
56
|
+
['heuristics.ios.logging.sensitive-data.ast']
|
|
57
|
+
),
|
|
50
58
|
'skills.ios.no-unchecked-sendable': heuristicDetector('ios.unchecked-sendable', [
|
|
51
59
|
'heuristics.ios.unchecked-sendable.ast',
|
|
52
60
|
]),
|
|
@@ -366,11 +366,15 @@ const resolveRuleSeverity = (params: {
|
|
|
366
366
|
stage: Exclude<GateStage, 'STAGED'>;
|
|
367
367
|
}): Severity => {
|
|
368
368
|
const promotedRuleIds = params.bundlePolicy?.promoteToErrorRuleIds ?? [];
|
|
369
|
-
const
|
|
369
|
+
const shouldPromote = promotedRuleIds.includes(params.rule.id);
|
|
370
|
+
|
|
371
|
+
if (
|
|
370
372
|
params.rule.id.endsWith('.no-solid-violations') &&
|
|
371
|
-
(params.stage === 'PRE_PUSH' || params.stage === 'CI')
|
|
372
|
-
|
|
373
|
-
|
|
373
|
+
(params.stage === 'PRE_PUSH' || params.stage === 'CI') &&
|
|
374
|
+
!shouldPromote
|
|
375
|
+
) {
|
|
376
|
+
return 'WARN';
|
|
377
|
+
}
|
|
374
378
|
|
|
375
379
|
if (!shouldPromote) {
|
|
376
380
|
return params.rule.severity;
|
|
@@ -111,6 +111,22 @@ const toLifecycleAuditFinding = (finding: SnapshotFinding): LifecycleAuditFindin
|
|
|
111
111
|
blocking: isFindingBlocking(finding),
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
const toGateAllowedAuditAdvisoryFinding = (
|
|
115
|
+
finding: LifecycleAuditFinding
|
|
116
|
+
): LifecycleAuditFinding => {
|
|
117
|
+
if (!finding.blocking) {
|
|
118
|
+
return finding;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
...finding,
|
|
122
|
+
severity: 'WARN',
|
|
123
|
+
blocking: false,
|
|
124
|
+
message:
|
|
125
|
+
`${finding.message} ` +
|
|
126
|
+
'(Advisory: current audit gate exited 0, so this finding is not blocking for this run.)',
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
114
130
|
const buildBlockedWithoutFindingsFallback = (params: {
|
|
115
131
|
stage: LifecycleAuditStage;
|
|
116
132
|
gateExitCode: number;
|
|
@@ -282,10 +298,15 @@ export const runLifecycleAudit = async (params: {
|
|
|
282
298
|
scope,
|
|
283
299
|
findings,
|
|
284
300
|
});
|
|
301
|
+
const gateAllowed = originalGateExitCode === 0;
|
|
285
302
|
const effectiveFindings = scopedGlobalEnforcementOnly
|
|
286
303
|
? findings.map(toScopedAuditAdvisoryFinding)
|
|
287
|
-
:
|
|
304
|
+
: gateAllowed
|
|
305
|
+
? findings.map(toGateAllowedAuditAdvisoryFinding)
|
|
306
|
+
: findings;
|
|
288
307
|
const gateExitCode = scopedGlobalEnforcementOnly ? 0 : originalGateExitCode;
|
|
308
|
+
const effectiveSnapshotOutcome =
|
|
309
|
+
gateExitCode === 0 && snapshotOutcome === 'BLOCK' ? 'PASS' : snapshotOutcome;
|
|
289
310
|
|
|
290
311
|
return {
|
|
291
312
|
command: 'pumuki audit',
|
|
@@ -299,7 +320,7 @@ export const runLifecycleAudit = async (params: {
|
|
|
299
320
|
gate_exit_code: gateExitCode,
|
|
300
321
|
files_scanned: filesScanned,
|
|
301
322
|
untracked_matching_extensions_count: untrackedMatchingExtensionsCount,
|
|
302
|
-
snapshot_outcome:
|
|
323
|
+
snapshot_outcome: effectiveSnapshotOutcome,
|
|
303
324
|
findings_count: effectiveFindings.length,
|
|
304
325
|
blocking_findings_count: effectiveFindings.filter((finding) => finding.blocking).length,
|
|
305
326
|
rules_coverage: evidence?.snapshot.rules_coverage ?? null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.192",
|
|
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": {
|