pumuki 6.3.321 → 6.3.323
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 +6 -0
- package/core/facts/detectors/text/ios.ts +24 -8
- package/core/facts/extractHeuristicFacts.ts +2 -2
- package/integrations/gate/evaluateAiGate.ts +3 -0
- package/package.json +1 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +17 -3
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +7 -3
|
@@ -171,8 +171,10 @@ import {
|
|
|
171
171
|
hasSwiftTestDoubleWithoutProtocolConformanceUsage,
|
|
172
172
|
hasSwiftSheetIsPresentedUsage,
|
|
173
173
|
hasSwiftScrollViewShowsIndicatorsUsage,
|
|
174
|
+
collectSwiftSensitiveLoggingLines,
|
|
174
175
|
hasSwiftSensitiveLoggingUsage,
|
|
175
176
|
hasSwiftSelfPrintChangesUsage,
|
|
177
|
+
collectSwiftSensitiveUserDefaultsStorageLines,
|
|
176
178
|
hasSwiftSensitiveUserDefaultsStorageUsage,
|
|
177
179
|
hasSwiftInsecureTransportUsage,
|
|
178
180
|
hasSwiftJSONSerializationUsage,
|
|
@@ -1944,7 +1946,9 @@ logger.error("Refresh failed \\(refreshToken)")
|
|
|
1944
1946
|
assert.equal(hasSwiftAdHocLoggingUsage(adHoc), true);
|
|
1945
1947
|
assert.equal(hasSwiftAdHocLoggingUsage(structuredSafe), false);
|
|
1946
1948
|
assert.equal(hasSwiftSensitiveLoggingUsage(sensitive), true);
|
|
1949
|
+
assert.deepEqual(collectSwiftSensitiveLoggingLines(sensitive), [2, 3]);
|
|
1947
1950
|
assert.equal(hasSwiftSensitiveLoggingUsage(structuredSafe), false);
|
|
1951
|
+
assert.deepEqual(collectSwiftSensitiveLoggingLines(structuredSafe), []);
|
|
1948
1952
|
});
|
|
1949
1953
|
|
|
1950
1954
|
test('hasSwiftHardcodedSensitiveStringUsage detecta secretos hardcodeados en Swift productivo', () => {
|
|
@@ -2045,7 +2049,9 @@ let text = "UserDefaults.standard.set(accessToken, forKey: \\"accessToken\\")"
|
|
|
2045
2049
|
`;
|
|
2046
2050
|
|
|
2047
2051
|
assert.equal(hasSwiftSensitiveUserDefaultsStorageUsage(source), true);
|
|
2052
|
+
assert.deepEqual(collectSwiftSensitiveUserDefaultsStorageLines(source), [2, 3]);
|
|
2048
2053
|
assert.equal(hasSwiftSensitiveUserDefaultsStorageUsage(ignored), false);
|
|
2054
|
+
assert.deepEqual(collectSwiftSensitiveUserDefaultsStorageLines(ignored), []);
|
|
2049
2055
|
});
|
|
2050
2056
|
|
|
2051
2057
|
test('detector iOS de seguridad detecta transporte inseguro HTTP y ATS permisivo', () => {
|
|
@@ -1624,7 +1624,12 @@ export const hasSwiftAdHocLoggingUsage = (source: string): boolean => {
|
|
|
1624
1624
|
};
|
|
1625
1625
|
|
|
1626
1626
|
export const hasSwiftSensitiveLoggingUsage = (source: string): boolean => {
|
|
1627
|
-
return source
|
|
1627
|
+
return collectSwiftSensitiveLoggingLines(source).length > 0;
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
export const collectSwiftSensitiveLoggingLines = (source: string): readonly number[] => {
|
|
1631
|
+
const lines: number[] = [];
|
|
1632
|
+
source.split(/\r?\n/).forEach((line, index) => {
|
|
1628
1633
|
const sanitized = stripSwiftLineForSemanticScan(line);
|
|
1629
1634
|
const lineWithoutComments = line.replace(/\/\/.*$/, '');
|
|
1630
1635
|
const hasLoggingCall =
|
|
@@ -1634,13 +1639,16 @@ export const hasSwiftSensitiveLoggingUsage = (source: string): boolean => {
|
|
|
1634
1639
|
);
|
|
1635
1640
|
|
|
1636
1641
|
if (!hasLoggingCall) {
|
|
1637
|
-
return
|
|
1642
|
+
return;
|
|
1638
1643
|
}
|
|
1639
1644
|
|
|
1640
|
-
|
|
1645
|
+
if (/\b(?:accessToken|refreshToken|authToken|token|password|secret|credential|authorization|email|userId)\b/i.test(
|
|
1641
1646
|
lineWithoutComments
|
|
1642
|
-
)
|
|
1647
|
+
)) {
|
|
1648
|
+
lines.push(index + 1);
|
|
1649
|
+
}
|
|
1643
1650
|
});
|
|
1651
|
+
return sortedUniqueLines(lines);
|
|
1644
1652
|
};
|
|
1645
1653
|
|
|
1646
1654
|
export const hasSwiftHardcodedSensitiveStringUsage = (source: string): boolean => {
|
|
@@ -1700,20 +1708,28 @@ export const hasSwiftJSONSerializationUsage = (source: string): boolean => {
|
|
|
1700
1708
|
};
|
|
1701
1709
|
|
|
1702
1710
|
export const hasSwiftSensitiveUserDefaultsStorageUsage = (source: string): boolean => {
|
|
1703
|
-
return source
|
|
1711
|
+
return collectSwiftSensitiveUserDefaultsStorageLines(source).length > 0;
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1714
|
+
export const collectSwiftSensitiveUserDefaultsStorageLines = (source: string): readonly number[] => {
|
|
1715
|
+
const lines: number[] = [];
|
|
1716
|
+
source.split(/\r?\n/).forEach((line, index) => {
|
|
1704
1717
|
const sanitized = stripSwiftLineForSemanticScan(line);
|
|
1705
1718
|
const lineWithoutComments = line.replace(/\/\/.*$/, '');
|
|
1706
1719
|
const hasUserDefaultsWrite = /\bUserDefaults\s*\.\s*standard\s*\.\s*set\s*\(/.test(sanitized);
|
|
1707
1720
|
const hasAppStorage = /@\s*AppStorage\s*\(/.test(sanitized);
|
|
1708
1721
|
|
|
1709
1722
|
if (!hasUserDefaultsWrite && !hasAppStorage) {
|
|
1710
|
-
return
|
|
1723
|
+
return;
|
|
1711
1724
|
}
|
|
1712
1725
|
|
|
1713
|
-
|
|
1726
|
+
if (/\b(?:accessToken|refreshToken|authToken|token|password|secret|credential|authorization|bearer|apiKey|sessionId)\b/i.test(
|
|
1714
1727
|
lineWithoutComments
|
|
1715
|
-
)
|
|
1728
|
+
)) {
|
|
1729
|
+
lines.push(index + 1);
|
|
1730
|
+
}
|
|
1716
1731
|
});
|
|
1732
|
+
return sortedUniqueLines(lines);
|
|
1717
1733
|
};
|
|
1718
1734
|
|
|
1719
1735
|
export const hasSwiftInsecureTransportUsage = (source: string): boolean => {
|
|
@@ -805,12 +805,12 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
805
805
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftMagicNumberLayoutUsage, locateLines: TextIOS.collectSwiftMagicNumberLayoutLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI layout numeric literal', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: named metric constant or design token', lines }], why: 'Inline numeric layout literals hide design intent and make visual remediation dependent on scanning the whole view body.', impact: 'Pixel-perfect slices can be blocked without a concrete node unless the finding points to the exact spacing, frame or padding call to fix.', expected_fix: 'Move repeated or meaningful layout numbers into named constants or design tokens, or use relative layout APIs when the number encodes screen geometry.', ruleId: 'heuristics.ios.maintainability.magic-number-layout.ast', code: 'HEURISTICS_IOS_MAINTAINABILITY_MAGIC_NUMBER_LAYOUT_AST', message: 'AST heuristic detected SwiftUI layout magic numbers; named constants or design tokens remain the preferred baseline.' },
|
|
806
806
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUIKitManualFrameLayoutUsage, locateLines: TextIOS.collectSwiftUIKitManualFrameLayoutLines, primaryNode: (lines) => ({ kind: 'call', name: 'UIKit manual frame CGRect layout', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: Auto Layout constraints or SwiftUI relative layout', lines }], why: 'Manual UIKit frames hard-code geometry instead of using Auto Layout constraints or SwiftUI relative layout, so the layout cannot adapt reliably to devices, Dynamic Type or localization.', impact: 'Pixel-perfect work can regress across screens because fixed CGRect values bypass constraint solving and produce file-level ambiguity unless the exact frame node is reported.', expected_fix: 'Replace UIView(frame: CGRect(...)) and .frame = CGRect(...) layout with Auto Layout anchors/NSLayoutConstraint, UIStackView constraints, or SwiftUI relative layout where the screen is SwiftUI-first.', ruleId: 'heuristics.ios.uikit.manual-frame-layout.ast', code: 'HEURISTICS_IOS_UIKIT_MANUAL_FRAME_LAYOUT_AST', message: 'AST heuristic detected UIKit manual frame layout; use Auto Layout constraints or SwiftUI relative layout.' },
|
|
807
807
|
{ 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.' },
|
|
808
|
-
{ 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.' },
|
|
808
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftSensitiveLoggingUsage, locateLines: TextIOS.collectSwiftSensitiveLoggingLines, primaryNode: (lines) => ({ kind: 'call', name: 'logging call with sensitive data', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: redacted structured log without token/password/email/userId', lines }], why: 'Production logs must not include tokens, credentials, emails or user identifiers because logs are copied to diagnostics, crash reports and external observability stores.', impact: 'Sensitive values can leak outside the device or backend trust boundary and make incident response depend on scrubbing historical logs.', expected_fix: 'Remove the sensitive value from the log, log a redacted marker, or emit structured metadata that cannot expose token, password, email, authorization or userId values.', 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.' },
|
|
809
809
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftHardcodedSensitiveStringUsage, locateLines: TextIOS.collectSwiftHardcodedSensitiveStringLines, primaryNode: (lines) => ({ kind: 'property', name: 'hardcoded sensitive Swift string', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: Keychain, secure config or environment-specific secret source', lines }], why: 'Sensitive strings assigned directly to token/password/secret properties create static production secrets and cannot be rotated safely.', impact: 'Credentials or identifiers can leak through source, binaries, logs or screenshots and block release until the concrete assignment is removed.', expected_fix: 'Read sensitive values from Keychain, secure configuration, injected environment or a repository-approved secret provider. Keep user-facing copy in localization assets, not sensitive variables.', ruleId: 'heuristics.ios.security.hardcoded-sensitive-string.ast', code: 'HEURISTICS_IOS_SECURITY_HARDCODED_SENSITIVE_STRING_AST', message: 'AST heuristic detected hardcoded sensitive Swift string; Keychain, secure config or environment-specific secrets remain the preferred baseline.' },
|
|
810
810
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUnlocalizedDateFormatterUsage, ruleId: 'heuristics.ios.localization.unlocalized-dateformatter.ast', code: 'HEURISTICS_IOS_LOCALIZATION_UNLOCALIZED_DATEFORMATTER_AST', message: 'AST heuristic detected DateFormatter dateFormat usage without an explicit locale.' },
|
|
811
811
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAlamofireUsage, ruleId: 'heuristics.ios.networking.alamofire.ast', code: 'HEURISTICS_IOS_NETWORKING_ALAMOFIRE_AST', message: 'AST heuristic detected Alamofire usage in iOS production code; URLSession remains the preferred baseline for new code.' },
|
|
812
812
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftJSONSerializationUsage, ruleId: 'heuristics.ios.json.jsonserialization.ast', code: 'HEURISTICS_IOS_JSON_JSONSERIALIZATION_AST', message: 'AST heuristic detected JSONSerialization usage in iOS production code; Codable remains the preferred baseline for new code.' },
|
|
813
|
-
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftSensitiveUserDefaultsStorageUsage, ruleId: 'heuristics.ios.security.userdefaults-sensitive-data.ast', code: 'HEURISTICS_IOS_SECURITY_USERDEFAULTS_SENSITIVE_DATA_AST', message: 'AST heuristic detected sensitive data stored in UserDefaults/AppStorage; native Keychain remains the preferred baseline for secrets.' },
|
|
813
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftSensitiveUserDefaultsStorageUsage, locateLines: TextIOS.collectSwiftSensitiveUserDefaultsStorageLines, primaryNode: (lines) => ({ kind: 'call', name: 'sensitive UserDefaults/AppStorage storage', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: native Keychain or secure secret store', lines }], why: 'Tokens, passwords, credentials and session identifiers must not be stored in UserDefaults or AppStorage because those stores are not the approved secret boundary.', impact: 'Secrets persisted in UserDefaults/AppStorage can leak through backups, diagnostics or simple local inspection, and the gate needs the exact storage node to avoid whole-file remediation.', expected_fix: 'Move tokens, passwords, credentials and session identifiers to native Keychain or the repository-approved secure storage adapter. Keep UserDefaults/AppStorage only for non-sensitive preferences.', ruleId: 'heuristics.ios.security.userdefaults-sensitive-data.ast', code: 'HEURISTICS_IOS_SECURITY_USERDEFAULTS_SENSITIVE_DATA_AST', message: 'AST heuristic detected sensitive data stored in UserDefaults/AppStorage; native Keychain remains the preferred baseline for secrets.' },
|
|
814
814
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftInsecureTransportUsage, ruleId: 'heuristics.ios.security.insecure-transport.ast', code: 'HEURISTICS_IOS_SECURITY_INSECURE_TRANSPORT_AST', message: 'AST heuristic detected insecure HTTP transport in iOS production code; HTTPS and ATS remain the preferred baseline.' },
|
|
815
815
|
{ platform: 'ios', pathCheck: isIOSInfoPlistPath, excludePaths: [], detect: TextIOS.hasSwiftInsecureTransportUsage, ruleId: 'heuristics.ios.security.insecure-transport.ast', code: 'HEURISTICS_IOS_SECURITY_INSECURE_TRANSPORT_AST', message: 'AST heuristic detected permissive App Transport Security configuration; HTTPS and ATS remain the preferred baseline.' },
|
|
816
816
|
{ platform: 'ios', pathCheck: isIOSLocalizableStringsPath, excludePaths: [], detect: detectsTrackedFilePresence, ruleId: 'heuristics.ios.localization.localizable-strings.ast', code: 'HEURISTICS_IOS_LOCALIZATION_LOCALIZABLE_STRINGS_AST', message: 'AST heuristic detected Localizable.strings usage; String Catalogs (.xcstrings) remain the preferred baseline for new localization work.' },
|
|
@@ -1696,6 +1696,9 @@ export const evaluateAiGate = (
|
|
|
1696
1696
|
suppressSkillsContractViolation
|
|
1697
1697
|
|| skillsContract.status !== 'FAIL'
|
|
1698
1698
|
|| (params.stage === 'PRE_WRITE' && requiredSkillsPlatforms.length === 0)
|
|
1699
|
+
|| skillsContract.violations.some((violation) =>
|
|
1700
|
+
violation.code === 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING'
|
|
1701
|
+
)
|
|
1699
1702
|
? []
|
|
1700
1703
|
: [
|
|
1701
1704
|
toSkillsViolation(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.323",
|
|
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": {
|
|
@@ -22,6 +22,16 @@ const buildBlockingCausesDetails = (
|
|
|
22
22
|
if (!causes || causes.length === 0) {
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
|
+
if (causes.every(isSkillsContractCause)) {
|
|
26
|
+
const first = resolvePrioritizedBlockingCauses(causes)[0];
|
|
27
|
+
if (!first) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
'Causas bloqueantes: 1',
|
|
32
|
+
...formatBlockingCauseForDialog(first, 0),
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
|
25
35
|
const total = causes.length;
|
|
26
36
|
const header = total === 1
|
|
27
37
|
? 'Causas bloqueantes: 1'
|
|
@@ -236,16 +246,20 @@ const formatPlatformName = (platform: string | null): string =>
|
|
|
236
246
|
: 'plataforma detectada';
|
|
237
247
|
|
|
238
248
|
const extractSkillsContractPlatform = (message: string): string | null =>
|
|
239
|
-
message.match(/\bplatforms?=([a-z0-9_,.-]+)/i)?.[1]?.split(',')[0]?.trim()
|
|
249
|
+
message.match(/\bplatforms?=([a-z0-9_,.-]+)/i)?.[1]?.split(',')[0]?.trim()
|
|
250
|
+
?? message.match(/\b(ios|android|frontend|backend)\(missing_critical_rule_ids=/i)?.[1]?.trim()
|
|
251
|
+
?? message.match(/\bfor\s+(ios|android|frontend|backend)\s*:/i)?.[1]?.trim()
|
|
252
|
+
?? null;
|
|
240
253
|
|
|
241
254
|
const extractSkillsContractRules = (message: string): string | null => {
|
|
242
255
|
const rulesMatch = message.match(/\brules?=\{([^}]+)\}/i)?.[1];
|
|
243
256
|
const missingMatch = message.match(/\bmissing=\[([^\]]+)\]/i)?.[1];
|
|
244
|
-
|
|
257
|
+
const criticalMissingMatch = message.match(/\bmissing_critical_rule_ids=\[([^\]]+)\]/i)?.[1];
|
|
258
|
+
return normalizeNotificationText(rulesMatch ?? criticalMissingMatch ?? missingMatch ?? '');
|
|
245
259
|
};
|
|
246
260
|
|
|
247
261
|
const SKILLS_CONTRACT_REMEDIATION =
|
|
248
|
-
'Actualiza o reconcilia el contrato de skills de Pumuki y vuelve a intentar el commit. Si estás en
|
|
262
|
+
'Actualiza o reconcilia el contrato de skills de Pumuki y vuelve a intentar el commit. Si ya estás en la última versión, es un bug del motor de Pumuki y debe corregirse allí antes de cerrar el commit.';
|
|
249
263
|
|
|
250
264
|
const WORKTREE_HYGIENE_REMEDIATION =
|
|
251
265
|
'Reduce el worktree pendiente a un slice atómico: stagea solo la tarea activa o guarda el resto en stash nombrado, y reejecuta PRE_WRITE.';
|
|
@@ -65,16 +65,20 @@ const formatPlatformName = (platform: string | null): string =>
|
|
|
65
65
|
: 'plataforma detectada';
|
|
66
66
|
|
|
67
67
|
const extractSkillsContractPlatform = (message: string): string | null =>
|
|
68
|
-
message.match(/\bplatforms?=([a-z0-9_,.-]+)/i)?.[1]?.split(',')[0]?.trim()
|
|
68
|
+
message.match(/\bplatforms?=([a-z0-9_,.-]+)/i)?.[1]?.split(',')[0]?.trim()
|
|
69
|
+
?? message.match(/\b(ios|android|frontend|backend)\(missing_critical_rule_ids=/i)?.[1]?.trim()
|
|
70
|
+
?? message.match(/\bfor\s+(ios|android|frontend|backend)\s*:/i)?.[1]?.trim()
|
|
71
|
+
?? null;
|
|
69
72
|
|
|
70
73
|
const extractSkillsContractRules = (message: string): string | null => {
|
|
71
74
|
const rulesMatch = message.match(/\brules?=\{([^}]+)\}/i)?.[1];
|
|
72
75
|
const missingMatch = message.match(/\bmissing=\[([^\]]+)\]/i)?.[1];
|
|
73
|
-
|
|
76
|
+
const criticalMissingMatch = message.match(/\bmissing_critical_rule_ids=\[([^\]]+)\]/i)?.[1];
|
|
77
|
+
return normalizeNotificationText(rulesMatch ?? criticalMissingMatch ?? missingMatch ?? '');
|
|
74
78
|
};
|
|
75
79
|
|
|
76
80
|
const SKILLS_CONTRACT_REMEDIATION =
|
|
77
|
-
'Actualiza o reconcilia el contrato de skills de Pumuki y vuelve a intentar el commit. Si estás en
|
|
81
|
+
'Actualiza o reconcilia el contrato de skills de Pumuki y vuelve a intentar el commit. Si ya estás en la última versión, es un bug del motor de Pumuki y debe corregirse allí antes de cerrar el commit.';
|
|
78
82
|
|
|
79
83
|
const extractExpectedBddFeatureFile = (cause: BlockedCause): string => {
|
|
80
84
|
const directFile = cause.file?.endsWith('.feature') ? cause.file : null;
|