pumuki 6.3.327 → 6.3.329
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.
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
collectSwiftAdHocLoggingLines,
|
|
38
38
|
hasSwiftAdHocLoggingUsage,
|
|
39
39
|
hasSwiftAlamofireUsage,
|
|
40
|
+
collectSwiftAlamofireLines,
|
|
40
41
|
collectSwiftComposableArchitectureUsageLines,
|
|
41
42
|
hasSwiftEmptyCatchUsage,
|
|
42
43
|
collectSwiftNSErrorThrowLines,
|
|
@@ -151,6 +152,7 @@ import {
|
|
|
151
152
|
collectSwiftOnChangeTaskLines,
|
|
152
153
|
collectSwiftOnChangeReadonlyVarLines,
|
|
153
154
|
collectSwiftStrongDelegateReferenceLines,
|
|
155
|
+
collectSwiftStrongSelfEscapingClosureLines,
|
|
154
156
|
hasSwiftOnChangeReadonlyVarUsage,
|
|
155
157
|
hasSwiftOnAppearTaskUsage,
|
|
156
158
|
collectSwiftOnAppearTaskLines,
|
|
@@ -181,6 +183,7 @@ import {
|
|
|
181
183
|
hasSwiftSensitiveUserDefaultsStorageUsage,
|
|
182
184
|
hasSwiftInsecureTransportUsage,
|
|
183
185
|
hasSwiftJSONSerializationUsage,
|
|
186
|
+
collectSwiftJSONSerializationLines,
|
|
184
187
|
hasSwiftExplicitColorStaticMemberUsage,
|
|
185
188
|
hasSwiftClosureBasedViewBuilderContentUsage,
|
|
186
189
|
collectSwiftForceUnwrapLines,
|
|
@@ -1681,6 +1684,7 @@ final class CartViewModel {
|
|
|
1681
1684
|
`;
|
|
1682
1685
|
|
|
1683
1686
|
assert.equal(hasSwiftStrongSelfEscapingClosureUsage(source), true);
|
|
1687
|
+
assert.deepEqual(collectSwiftStrongSelfEscapingClosureLines(source), [7, 10, 13, 16, 19]);
|
|
1684
1688
|
});
|
|
1685
1689
|
|
|
1686
1690
|
test('hasSwiftStrongSelfEscapingClosureUsage preserva capture lists weak/unowned e ignora comentarios y strings', () => {
|
|
@@ -1703,6 +1707,7 @@ final class CartViewModel {
|
|
|
1703
1707
|
`;
|
|
1704
1708
|
|
|
1705
1709
|
assert.equal(hasSwiftStrongSelfEscapingClosureUsage(source), false);
|
|
1710
|
+
assert.deepEqual(collectSwiftStrongSelfEscapingClosureLines(source), []);
|
|
1706
1711
|
});
|
|
1707
1712
|
|
|
1708
1713
|
test('hasSwiftCustomSingletonUsage detecta singletons propios y excluye usos de singletons del sistema', () => {
|
|
@@ -2039,8 +2044,12 @@ final class APIClient {
|
|
|
2039
2044
|
|
|
2040
2045
|
assert.equal(hasSwiftAlamofireUsage(source), true);
|
|
2041
2046
|
assert.equal(hasSwiftJSONSerializationUsage(source), true);
|
|
2047
|
+
assert.deepEqual(collectSwiftAlamofireLines(source), [2, 6]);
|
|
2048
|
+
assert.deepEqual(collectSwiftJSONSerializationLines(source), [7]);
|
|
2042
2049
|
assert.equal(hasSwiftAlamofireUsage(ignored), false);
|
|
2043
2050
|
assert.equal(hasSwiftJSONSerializationUsage(ignored), false);
|
|
2051
|
+
assert.deepEqual(collectSwiftAlamofireLines(ignored), []);
|
|
2052
|
+
assert.deepEqual(collectSwiftJSONSerializationLines(ignored), []);
|
|
2044
2053
|
});
|
|
2045
2054
|
|
|
2046
2055
|
test('detector iOS de seguridad detecta secretos en UserDefaults y AppStorage', () => {
|
|
@@ -1498,6 +1498,7 @@ const swiftStrongSelfEscapingClosurePatterns = [
|
|
|
1498
1498
|
/\bDispatchQueue\s*\.\s*[A-Za-z0-9_.]+\s*\.\s*async(?:After)?\s*(?:\([^)]*\))?\s*\{/g,
|
|
1499
1499
|
/\bTimer\s*\.\s*scheduledTimer\s*\([\s\S]{0,320}?\)\s*\{/g,
|
|
1500
1500
|
/\bNotificationCenter\s*\.\s*default\s*\.\s*addObserver\s*\([\s\S]{0,420}?\busing\s*:\s*\{/g,
|
|
1501
|
+
/\bNotificationCenter\s*\.\s*default\s*\.\s*addObserver\s*\([\s\S]{0,420}?\)\s*\{/g,
|
|
1501
1502
|
/\.\s*sink\s*\([\s\S]{0,420}?\b(?:receiveValue|receiveCompletion)\s*:\s*\{/g,
|
|
1502
1503
|
/\.\s*sink\s*\{/g,
|
|
1503
1504
|
/\.\s*handleEvents\s*\([\s\S]{0,420}?\b(?:receiveOutput|receiveCompletion|receiveCancel)\s*:\s*\{/g,
|
|
@@ -1562,6 +1563,44 @@ export const hasSwiftStrongSelfEscapingClosureUsage = (source: string): boolean
|
|
|
1562
1563
|
return false;
|
|
1563
1564
|
};
|
|
1564
1565
|
|
|
1566
|
+
const toSwiftLineNumberAtOffset = (source: string, offset: number): number =>
|
|
1567
|
+
source.slice(0, Math.max(0, offset)).split(/\r?\n/).length;
|
|
1568
|
+
|
|
1569
|
+
export const collectSwiftStrongSelfEscapingClosureLines = (source: string): readonly number[] => {
|
|
1570
|
+
const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
|
|
1571
|
+
const lines: number[] = [];
|
|
1572
|
+
|
|
1573
|
+
for (const pattern of swiftStrongSelfEscapingClosurePatterns) {
|
|
1574
|
+
pattern.lastIndex = 0;
|
|
1575
|
+
for (const match of sanitized.matchAll(pattern)) {
|
|
1576
|
+
const matchedSource = match[0] ?? '';
|
|
1577
|
+
const openingBraceOffset = matchedSource.lastIndexOf('{');
|
|
1578
|
+
if (openingBraceOffset < 0 || match.index === undefined) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
const openingBraceIndex = match.index + openingBraceOffset;
|
|
1582
|
+
const closingBraceIndex = findMatchingSwiftBraceIndex(sanitized, openingBraceIndex);
|
|
1583
|
+
if (closingBraceIndex < 0) {
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
const closureBodyStartIndex = openingBraceIndex + 1;
|
|
1587
|
+
const closureBody = sanitized.slice(closureBodyStartIndex, closingBraceIndex);
|
|
1588
|
+
if (hasWeakOrUnownedSelfCaptureList(closureBody)) {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const strongSelfMatches = closureBody.matchAll(/\bself\s*\./g);
|
|
1592
|
+
for (const strongSelfMatch of strongSelfMatches) {
|
|
1593
|
+
if (strongSelfMatch.index === undefined) {
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
lines.push(toSwiftLineNumberAtOffset(sanitized, closureBodyStartIndex + strongSelfMatch.index));
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
return sortedUniqueLines(lines);
|
|
1602
|
+
};
|
|
1603
|
+
|
|
1565
1604
|
export const hasSwiftCustomSingletonUsage = (source: string): boolean => {
|
|
1566
1605
|
const singletonDeclarationPattern =
|
|
1567
1606
|
/^\s*(?:(?:private|fileprivate|internal|public|open)\s+)?static\s+(?:let|var)\s+shared\b(?:\s*:\s*[A-Za-z_][A-Za-z0-9_.<>]*)?\s*=/;
|
|
@@ -1719,15 +1758,23 @@ export const hasSwiftUnlocalizedDateFormatterUsage = (source: string): boolean =
|
|
|
1719
1758
|
return false;
|
|
1720
1759
|
};
|
|
1721
1760
|
|
|
1761
|
+
export const collectSwiftAlamofireLines = (source: string): readonly number[] => {
|
|
1762
|
+
return sortedUniqueLines([
|
|
1763
|
+
...collectSwiftRegexLines(source, /^\s*import\s+Alamofire\b/),
|
|
1764
|
+
...collectSwiftRegexLines(source, /\b(?:AF|Alamofire)\s*\.\s*request\b/),
|
|
1765
|
+
]);
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1722
1768
|
export const hasSwiftAlamofireUsage = (source: string): boolean => {
|
|
1723
|
-
return (
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1769
|
+
return collectSwiftAlamofireLines(source).length > 0;
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1772
|
+
export const collectSwiftJSONSerializationLines = (source: string): readonly number[] => {
|
|
1773
|
+
return collectSwiftRegexLines(source, /\bJSONSerialization\s*\./);
|
|
1727
1774
|
};
|
|
1728
1775
|
|
|
1729
1776
|
export const hasSwiftJSONSerializationUsage = (source: string): boolean => {
|
|
1730
|
-
return
|
|
1777
|
+
return collectSwiftJSONSerializationLines(source).length > 0;
|
|
1731
1778
|
};
|
|
1732
1779
|
|
|
1733
1780
|
export const hasSwiftSensitiveUserDefaultsStorageUsage = (source: string): boolean => {
|
|
@@ -789,7 +789,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
789
789
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnChangeTaskUsage, locateLines: TextIOS.collectSwiftOnChangeTaskLines, primaryNode: (lines) => ({ kind: 'call', name: 'Task launched inside SwiftUI onChange', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .task(id:) for value-dependent async work', lines }], why: 'A Task launched from onChange makes value-dependent async work manual instead of tying cancellation to the changing value.', impact: 'Search, load or refresh work can race after state changes because cancellation is not expressed through SwiftUI .task(id:).', expected_fix: 'Move value-dependent async work from .onChange { Task { ... } } into .task(id: value) { ... }. Keep onChange only for synchronous derivations or analytics.', ruleId: 'heuristics.ios.swiftui.onchange-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONCHANGE_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onChange; .task(id:) provides lifecycle-aware cancellation for value-dependent async work.' },
|
|
790
790
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnChangeReadonlyVarUsage, locateLines: TextIOS.collectSwiftOnChangeReadonlyVarLines, primaryNode: (lines) => ({ kind: 'property', name: 'var declared inside SwiftUI onChange closure', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: let for read-only derived value inside onChange', lines }], why: 'A local var inside onChange hides whether the closure is deriving a read-only value or mutating state as part of a reactive update.', impact: 'Reactive closures become harder to audit because unnecessary mutability can mask accidental state changes and value-dependent side effects.', expected_fix: 'Use let for read-only derived values inside onChange. Keep var only when the local value is intentionally mutated and extract complex mutation out of the view closure.', ruleId: 'heuristics.ios.swiftui.onchange-readonly-var.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONCHANGE_READONLY_VAR_AST', message: 'AST heuristic detected local var inside SwiftUI onChange; prefer let for read-only derived values.' },
|
|
791
791
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongDelegateReferenceUsage, locateLines: TextIOS.collectSwiftStrongDelegateReferenceLines, primaryNode: (lines) => ({ kind: 'property', name: 'strong delegate or dataSource property', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: weak var delegate/dataSource', lines }], why: 'Delegate and dataSource references usually point back to an owner or coordinator and should not be retained strongly by the callee.', impact: 'A strong delegate/dataSource property can create retain cycles between services, coordinators, view controllers or adapters, causing leaks that tests may not catch.', expected_fix: 'Declare delegate/dataSource references as weak var delegate or weak var dataSource, and keep the protocol class-bound with AnyObject when Swift requires weak storage.', ruleId: 'heuristics.ios.memory.strong-delegate.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_DELEGATE_AST', message: 'AST heuristic detected a strong delegate/dataSource reference; weak delegates remain the preferred baseline to avoid retain cycles.' },
|
|
792
|
-
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongSelfEscapingClosureUsage, ruleId: 'heuristics.ios.memory.strong-self-escaping-closure.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_SELF_ESCAPING_CLOSURE_AST', message: 'AST heuristic detected strong self capture in an escaping iOS closure; weak or unowned captures remain the preferred baseline when ownership is not explicit.' },
|
|
792
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongSelfEscapingClosureUsage, locateLines: TextIOS.collectSwiftStrongSelfEscapingClosureLines, primaryNode: (lines) => ({ kind: 'call', name: 'escaping closure captures self strongly', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: capture [weak self] or explicit lifetime owner', lines }], why: 'Escaping closures can outlive the object that created them, so capturing self strongly keeps the owner alive unless lifetime ownership is explicit.', impact: 'Tasks, timers, NotificationCenter observers and Combine sinks can retain view models, coordinators or services and produce leaks that only appear after navigation or cancellation paths.', expected_fix: 'Add an explicit capture list such as [weak self] and unwrap self safely inside the closure, or document and encode a deliberate owner lifetime when a strong capture is required.', ruleId: 'heuristics.ios.memory.strong-self-escaping-closure.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_SELF_ESCAPING_CLOSURE_AST', message: 'AST heuristic detected strong self capture in an escaping iOS closure; weak or unowned captures remain the preferred baseline when ownership is not explicit.' },
|
|
793
793
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUnownedSelfCaptureUsage, ruleId: 'heuristics.ios.memory.unowned-self-capture.ast', code: 'HEURISTICS_IOS_MEMORY_UNOWNED_SELF_CAPTURE_AST', message: 'AST heuristic detected unowned capture in an iOS closure; use weak capture unless lifetime is explicitly guaranteed.' },
|
|
794
794
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftManualMemoryManagementUsage, locateLines: TextIOS.collectSwiftManualMemoryManagementLines, primaryNode: (lines) => ({ kind: 'call', name: 'manual ARC bypass / Core Foundation retain-release', lines }), relatedNodes: (lines) => [{ kind: 'class', name: 'replacement: ARC-owned Swift object lifetime', lines }], why: 'Manual Unmanaged or Core Foundation retain/release bypasses normal Swift ARC ownership and needs an explicit bridge boundary.', impact: 'Manual memory ownership can leak or over-release objects, and without line evidence the gate cannot identify the unsafe bridge call.', expected_fix: 'Prefer ARC-owned Swift references. If a Core Foundation bridge is unavoidable, isolate it in infrastructure with documented ownership invariants and typed wrappers.', ruleId: 'heuristics.ios.memory.manual-management.ast', code: 'HEURISTICS_IOS_MEMORY_MANUAL_MANAGEMENT_AST', message: 'AST heuristic detected manual memory management that bypasses Swift ARC.' },
|
|
795
795
|
{ platform: 'ios', pathCheck: isSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage, locateLines: TextIOS.collectSwiftDirectSUTInstantiationWithoutMakeSUTLines, primaryNode: (lines) => ({ kind: 'call', name: 'direct let sut = Type(...) instantiation', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: makeSUT() factory', lines }], why: 'iOS tests that instantiate the SUT inline duplicate setup and bypass the repository makeSUT factory contract.', impact: 'Test setup drifts across methods, memory tracking and dependency wiring become inconsistent, and later brownfield quality checks cannot rely on a single factory boundary.', expected_fix: 'Move direct SUT construction into a private makeSUT() helper and let test methods call let sut = makeSUT(). Add trackForMemoryLeaks(sut) inside the factory when the repository memory contract requires it.', ruleId: 'heuristics.ios.testing.direct-sut-instantiation-without-makesut.ast', code: 'HEURISTICS_IOS_TESTING_DIRECT_SUT_INSTANTIATION_WITHOUT_MAKESUT_AST', message: 'AST heuristic detected direct SUT instantiation in an iOS test without makeSUT().' },
|
|
@@ -808,8 +808,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
808
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
|
-
{ 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
|
-
{ 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.' },
|
|
811
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAlamofireUsage, locateLines: TextIOS.collectSwiftAlamofireLines, primaryNode: (lines) => ({ kind: 'call', name: 'Alamofire import or request call', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: native URLSession networking boundary', lines }], why: 'iOS production networking should use the native URLSession boundary unless the repository explicitly approves a third-party networking dependency.', impact: 'Third-party networking calls create a separate policy surface for retries, cancellation, privacy and observability, and whole-file findings are not actionable during consumer remediation.', expected_fix: 'Replace Alamofire imports and AF/Alamofire request calls with the repository-approved URLSession client or a native networking adapter.', 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
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftJSONSerializationUsage, locateLines: TextIOS.collectSwiftJSONSerializationLines, primaryNode: (lines) => ({ kind: 'call', name: 'JSONSerialization call', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: Codable decoder or encoder', lines }], why: 'JSONSerialization bypasses typed decoding and makes payload contracts harder to validate than Codable models.', impact: 'Runtime casts and dictionary traversal can hide API drift until production and make the gate report vague file-level serialization debt.', expected_fix: 'Replace JSONSerialization calls with Codable DTOs using JSONDecoder or JSONEncoder at the repository networking boundary.', 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
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, locateLines: TextIOS.collectSwiftInsecureTransportLines, primaryNode: (lines) => ({ kind: 'call', name: 'insecure HTTP URL literal', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: HTTPS URLSession endpoint with ATS enabled', lines }], why: 'Production iOS networking must use HTTPS and keep App Transport Security enabled by default.', impact: 'HTTP endpoints can expose credentials, session data or payloads in transit and create release-blocking transport exceptions.', expected_fix: 'Replace http:// endpoints with https:// endpoints and keep ATS enabled. If a temporary exception is unavoidable, isolate it to a documented debug-only configuration, not production code.', 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.' },
|
|
815
815
|
{ platform: 'ios', pathCheck: isIOSInfoPlistPath, excludePaths: [], detect: TextIOS.hasSwiftInsecureTransportUsage, locateLines: TextIOS.collectSwiftInsecureTransportLines, primaryNode: (lines) => ({ kind: 'property', name: 'ATS NSAllowsArbitraryLoads enabled', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: ATS enabled with explicit HTTPS-only exceptions', lines }], why: 'NSAllowsArbitraryLoads disables the default iOS App Transport Security protection and should not be enabled for production builds.', impact: 'Permissive ATS configuration allows insecure transport globally, making networking violations harder to audit per endpoint.', expected_fix: 'Remove NSAllowsArbitraryLoads=true. Keep ATS enabled by default and add the narrowest documented domain exception only when the production requirement is approved.', ruleId: 'heuristics.ios.security.insecure-transport.ast', code: 'HEURISTICS_IOS_SECURITY_INSECURE_TRANSPORT_AST', message: 'AST heuristic detected permissive App Transport Security configuration.' },
|
package/core/facts/index.test.ts
CHANGED
|
@@ -100,3 +100,80 @@ final class SystemPermissionsService {
|
|
|
100
100
|
assert.deepEqual(match.primary_node?.lines, [3]);
|
|
101
101
|
assert.match(match.expected_fix ?? '', /weak var delegate/i);
|
|
102
102
|
});
|
|
103
|
+
|
|
104
|
+
test('extractHeuristicFacts emite strong self iOS con linea y remediation accionable', () => {
|
|
105
|
+
const params: HeuristicExtractionParams = {
|
|
106
|
+
facts: [
|
|
107
|
+
{
|
|
108
|
+
kind: 'FileContent',
|
|
109
|
+
source: 'repo',
|
|
110
|
+
path: 'apps/ios/Presentation/Features/Cart/CartViewModel.swift',
|
|
111
|
+
content: `
|
|
112
|
+
final class CartViewModel {
|
|
113
|
+
func bind() {
|
|
114
|
+
Task {
|
|
115
|
+
await self.reload()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
detectedPlatforms: {
|
|
123
|
+
ios: { detected: true, confidence: 'HIGH' },
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const extractedFacts = extractHeuristicFacts(params);
|
|
128
|
+
const match = extractedFacts.find(
|
|
129
|
+
(fact) => fact.ruleId === 'heuristics.ios.memory.strong-self-escaping-closure.ast'
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
assert.ok(match);
|
|
133
|
+
assert.deepEqual(match.lines, [5]);
|
|
134
|
+
assert.deepEqual(match.primary_node?.lines, [5]);
|
|
135
|
+
assert.match(match.expected_fix ?? '', /\[weak self\]/i);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('extractHeuristicFacts emite networking iOS con lineas y remediation accionable', () => {
|
|
139
|
+
const params: HeuristicExtractionParams = {
|
|
140
|
+
facts: [
|
|
141
|
+
{
|
|
142
|
+
kind: 'FileContent',
|
|
143
|
+
source: 'repo',
|
|
144
|
+
path: 'apps/ios/Infrastructure/Networking/BuyerAPIClient.swift',
|
|
145
|
+
content: `
|
|
146
|
+
import Alamofire
|
|
147
|
+
|
|
148
|
+
final class BuyerAPIClient {
|
|
149
|
+
func load() {
|
|
150
|
+
AF.request("https://api.example.com")
|
|
151
|
+
let object = try? JSONSerialization.jsonObject(with: Data())
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
`,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
detectedPlatforms: {
|
|
158
|
+
ios: { detected: true, confidence: 'HIGH' },
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const extractedFacts = extractHeuristicFacts(params);
|
|
163
|
+
const alamofire = extractedFacts.find(
|
|
164
|
+
(fact) => fact.ruleId === 'heuristics.ios.networking.alamofire.ast'
|
|
165
|
+
);
|
|
166
|
+
const jsonSerialization = extractedFacts.find(
|
|
167
|
+
(fact) => fact.ruleId === 'heuristics.ios.json.jsonserialization.ast'
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
assert.ok(alamofire);
|
|
171
|
+
assert.deepEqual(alamofire.lines, [2, 6]);
|
|
172
|
+
assert.deepEqual(alamofire.primary_node?.lines, [2, 6]);
|
|
173
|
+
assert.match(alamofire.expected_fix ?? '', /URLSession/i);
|
|
174
|
+
|
|
175
|
+
assert.ok(jsonSerialization);
|
|
176
|
+
assert.deepEqual(jsonSerialization.lines, [7]);
|
|
177
|
+
assert.deepEqual(jsonSerialization.primary_node?.lines, [7]);
|
|
178
|
+
assert.match(jsonSerialization.expected_fix ?? '', /Codable/i);
|
|
179
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.329",
|
|
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": {
|