pumuki 6.3.358 → 6.3.360
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/android.test.ts +35 -0
- package/core/facts/detectors/text/android.ts +22 -0
- package/core/facts/extractHeuristicFacts.ts +1 -0
- package/core/rules/presets/heuristics/android.test.ts +6 -1
- package/core/rules/presets/heuristics/android.ts +20 -0
- package/integrations/config/skillsDetectorRegistry.ts +4 -0
- package/package.json +1 -1
- package/scripts/framework-menu-system-notifications-cause.ts +5 -0
- package/scripts/framework-menu-system-notifications-legacy-skill-cause.ts +107 -0
- package/scripts/framework-menu-system-notifications-remediation.ts +5 -0
- package/skills.lock.json +1 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
findKotlinOpenClosedWhenMatch,
|
|
8
8
|
findKotlinPresentationSrpMatch,
|
|
9
9
|
hasAndroidAsyncTaskUsage,
|
|
10
|
+
hasAndroidLegacyFingerprintApiUsage,
|
|
10
11
|
hasKotlinCoroutineTryCatchUsage,
|
|
11
12
|
hasKotlinDispatcherMainBoundaryLeakUsage,
|
|
12
13
|
hasKotlinGlobalScopeUsage,
|
|
@@ -1243,3 +1244,37 @@ class PumukiLspAndroidCanaryPremiumDiscountPolicy : PumukiLspAndroidCanaryDiscou
|
|
|
1243
1244
|
assert.match(match.impact, /sustituci|regresion|crash/i);
|
|
1244
1245
|
assert.match(match.expected_fix, /contrato base|estrategia|subtipo/i);
|
|
1245
1246
|
});
|
|
1247
|
+
|
|
1248
|
+
test('hasAndroidLegacyFingerprintApiUsage detecta APIs biométricas legacy y preserva BiometricPrompt', () => {
|
|
1249
|
+
const legacyManager = `
|
|
1250
|
+
import android.hardware.fingerprint.FingerprintManager
|
|
1251
|
+
|
|
1252
|
+
class LegacyBiometricAuth(private val fingerprintManager: FingerprintManager) {
|
|
1253
|
+
fun authenticate(callback: FingerprintManager.AuthenticationCallback) {
|
|
1254
|
+
fingerprintManager.authenticate(null, null, 0, callback, null)
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
`;
|
|
1258
|
+
const legacyCompat = `
|
|
1259
|
+
class LegacyCompatAuth(private val fingerprintManager: FingerprintManagerCompat) {
|
|
1260
|
+
fun authenticate(callback: FingerprintManagerCompat.AuthenticationCallback) {
|
|
1261
|
+
fingerprintManager.authenticate(null, 0, null, callback, null)
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
`;
|
|
1265
|
+
const modern = `
|
|
1266
|
+
import androidx.biometric.BiometricPrompt
|
|
1267
|
+
|
|
1268
|
+
class ModernBiometricAuth(private val biometricPrompt: BiometricPrompt) {
|
|
1269
|
+
fun authenticate(promptInfo: BiometricPrompt.PromptInfo) {
|
|
1270
|
+
biometricPrompt.authenticate(promptInfo)
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
// FingerprintManager should not match inside comments
|
|
1274
|
+
val sample = "FingerprintManagerCompat"
|
|
1275
|
+
`;
|
|
1276
|
+
|
|
1277
|
+
assert.equal(hasAndroidLegacyFingerprintApiUsage(legacyManager), true);
|
|
1278
|
+
assert.equal(hasAndroidLegacyFingerprintApiUsage(legacyCompat), true);
|
|
1279
|
+
assert.equal(hasAndroidLegacyFingerprintApiUsage(modern), false);
|
|
1280
|
+
});
|
|
@@ -1407,3 +1407,25 @@ export const findKotlinLiskovSubstitutionMatch = (
|
|
|
1407
1407
|
|
|
1408
1408
|
return undefined;
|
|
1409
1409
|
};
|
|
1410
|
+
|
|
1411
|
+
const legacyFingerprintApiPattern =
|
|
1412
|
+
/\b(?:android\.hardware\.fingerprint\.)?FingerprintManager(?:Compat)?\b/;
|
|
1413
|
+
|
|
1414
|
+
export const hasAndroidLegacyFingerprintApiUsage = (source: string): boolean =>
|
|
1415
|
+
scanCodeLikeSource(source, ({ source: scannedSource, index }) => {
|
|
1416
|
+
const match = scannedSource.slice(index).match(legacyFingerprintApiPattern);
|
|
1417
|
+
return match?.index === 0;
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
export const collectAndroidLegacyFingerprintApiLines = (
|
|
1421
|
+
source: string
|
|
1422
|
+
): readonly number[] => {
|
|
1423
|
+
const lines: number[] = [];
|
|
1424
|
+
source.split(/\r?\n/).forEach((line, index) => {
|
|
1425
|
+
const sanitized = stripKotlinLineForSemanticScan(line);
|
|
1426
|
+
if (legacyFingerprintApiPattern.test(sanitized)) {
|
|
1427
|
+
lines.push(index + 1);
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
return sortedUniqueLines(lines);
|
|
1431
|
+
};
|
|
@@ -893,6 +893,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
893
893
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinGlobalScopeUsage, ruleId: 'heuristics.android.globalscope.ast', code: 'HEURISTICS_ANDROID_GLOBAL_SCOPE_AST', message: 'AST heuristic detected GlobalScope coroutine usage in production Kotlin code.' },
|
|
894
894
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinRunBlockingUsage, ruleId: 'heuristics.android.run-blocking.ast', code: 'HEURISTICS_ANDROID_RUN_BLOCKING_AST', message: 'AST heuristic detected runBlocking usage in production Kotlin code.' },
|
|
895
895
|
{ platform: 'android', pathCheck: isAndroidSourcePath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidAsyncTaskUsage, ruleId: 'heuristics.android.concurrency.asynctask.ast', code: 'HEURISTICS_ANDROID_CONCURRENCY_ASYNCTASK_AST', message: 'AST heuristic detected deprecated AsyncTask usage in Android production code; use coroutines.' },
|
|
896
|
+
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidLegacyFingerprintApiUsage, locateLines: TextAndroid.collectAndroidLegacyFingerprintApiLines, ruleId: 'heuristics.android.security.legacy-fingerprint-api.ast', code: 'HEURISTICS_ANDROID_SECURITY_LEGACY_FINGERPRINT_API_AST', message: 'AST heuristic detected legacy FingerprintManager API usage; use androidx.biometric.BiometricPrompt.' },
|
|
896
897
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinGodActivityUsage, ruleId: 'heuristics.android.architecture.god-activity.ast', code: 'HEURISTICS_ANDROID_ARCHITECTURE_GOD_ACTIVITY_AST', message: 'AST heuristic detected an Android Activity mixing UI entrypoint with product responsibilities; keep Activity thin and move features to composables/ViewModels/use cases.' },
|
|
897
898
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinApplicationOnCreateHeavyInitializationUsage, ruleId: 'heuristics.android.startup.application-oncreate-heavy-init.ast', code: 'HEURISTICS_ANDROID_STARTUP_APPLICATION_ONCREATE_HEAVY_INIT_AST', message: 'AST heuristic detected heavy library initialization in Application.onCreate; move lazy startup work to AndroidX Startup Initializer or defer it behind the feature boundary.' },
|
|
898
899
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinNonLazyScrollableCollectionUsage, ruleId: 'heuristics.android.compose.non-lazy-scrollable-collection.ast', code: 'HEURISTICS_ANDROID_COMPOSE_NON_LAZY_SCROLLABLE_COLLECTION_AST', message: 'AST heuristic detected a scrollable Column/Row rendering a collection; use LazyColumn/LazyRow for virtualized lists.' },
|
|
@@ -3,7 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import { androidRules } from './android';
|
|
4
4
|
|
|
5
5
|
test('androidRules define reglas heurísticas locked para plataforma android', () => {
|
|
6
|
-
assert.equal(androidRules.length,
|
|
6
|
+
assert.equal(androidRules.length, 40);
|
|
7
7
|
|
|
8
8
|
const ids = androidRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -13,6 +13,7 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
13
13
|
'heuristics.android.flow.livedata-state-exposure.ast',
|
|
14
14
|
'heuristics.android.flow.viewmodel-flow-without-statein.ast',
|
|
15
15
|
'heuristics.android.flow.sharedflow-used-as-state.ast',
|
|
16
|
+
'heuristics.android.security.legacy-fingerprint-api.ast',
|
|
16
17
|
'heuristics.android.concurrency.asynctask.ast',
|
|
17
18
|
'heuristics.android.architecture.god-activity.ast',
|
|
18
19
|
'heuristics.android.startup.application-oncreate-heavy-init.ast',
|
|
@@ -83,6 +84,10 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
83
84
|
byId.get('heuristics.android.flow.sharedflow-used-as-state.ast')?.then.code,
|
|
84
85
|
'HEURISTICS_ANDROID_FLOW_SHAREDFLOW_USED_AS_STATE_AST'
|
|
85
86
|
);
|
|
87
|
+
assert.equal(
|
|
88
|
+
byId.get('heuristics.android.security.legacy-fingerprint-api.ast')?.then.code,
|
|
89
|
+
'HEURISTICS_ANDROID_SECURITY_LEGACY_FINGERPRINT_API_AST'
|
|
90
|
+
);
|
|
86
91
|
assert.equal(
|
|
87
92
|
byId.get('heuristics.android.concurrency.asynctask.ast')?.then.code,
|
|
88
93
|
'HEURISTICS_ANDROID_CONCURRENCY_ASYNCTASK_AST'
|
|
@@ -115,6 +115,26 @@ export const androidRules: RuleSet = [
|
|
|
115
115
|
code: 'HEURISTICS_ANDROID_FLOW_SHAREDFLOW_USED_AS_STATE_AST',
|
|
116
116
|
},
|
|
117
117
|
},
|
|
118
|
+
{
|
|
119
|
+
id: 'heuristics.android.security.legacy-fingerprint-api.ast',
|
|
120
|
+
description:
|
|
121
|
+
'Detects legacy FingerprintManager API usage where androidx.biometric.BiometricPrompt is the supported baseline.',
|
|
122
|
+
severity: 'WARN',
|
|
123
|
+
platform: 'android',
|
|
124
|
+
locked: true,
|
|
125
|
+
when: {
|
|
126
|
+
kind: 'Heuristic',
|
|
127
|
+
where: {
|
|
128
|
+
ruleId: 'heuristics.android.security.legacy-fingerprint-api.ast',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
then: {
|
|
132
|
+
kind: 'Finding',
|
|
133
|
+
message:
|
|
134
|
+
'AST heuristic detected legacy FingerprintManager API usage; use androidx.biometric.BiometricPrompt.',
|
|
135
|
+
code: 'HEURISTICS_ANDROID_SECURITY_LEGACY_FINGERPRINT_API_AST',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
118
138
|
{
|
|
119
139
|
id: 'heuristics.android.concurrency.asynctask.ast',
|
|
120
140
|
description:
|
|
@@ -1431,6 +1431,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
1431
1431
|
'android.flow.state-exposure',
|
|
1432
1432
|
['heuristics.android.flow.livedata-state-exposure.ast']
|
|
1433
1433
|
),
|
|
1434
|
+
'skills.android.guideline.android.biometric-auth-biometricprompt-api': heuristicDetector(
|
|
1435
|
+
'android.security.biometric-prompt',
|
|
1436
|
+
['heuristics.android.security.legacy-fingerprint-api.ast']
|
|
1437
|
+
),
|
|
1434
1438
|
'skills.android.guideline.android.statein-convertir-cold-flow-a-hot-stateflow': heuristicDetector(
|
|
1435
1439
|
'android.flow.viewmodel-flow-without-statein',
|
|
1436
1440
|
['heuristics.android.flow.viewmodel-flow-without-statein.ast']
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.360",
|
|
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": {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
buildNotificationTrackingCauseSummary,
|
|
8
8
|
extractNotificationTrackingContext,
|
|
9
9
|
} from './framework-menu-system-notifications-tracking';
|
|
10
|
+
import { buildLegacySkillCauseSummary } from './framework-menu-system-notifications-legacy-skill-cause';
|
|
10
11
|
|
|
11
12
|
const BLOCKED_CAUSE_SUMMARY_BY_CODE: Readonly<Record<string, string>> = {
|
|
12
13
|
EVIDENCE_GATE_BLOCKED: 'El gate de evidencia/gobernanza está bloqueado.',
|
|
@@ -170,6 +171,10 @@ export const resolveBlockedCauseSummary = (
|
|
|
170
171
|
if (blockedCausesSummary) {
|
|
171
172
|
return truncateNotificationText(blockedCausesSummary, 72);
|
|
172
173
|
}
|
|
174
|
+
const legacySkillCauseSummary = buildLegacySkillCauseSummary(event.causeMessage);
|
|
175
|
+
if (legacySkillCauseSummary) {
|
|
176
|
+
return legacySkillCauseSummary;
|
|
177
|
+
}
|
|
173
178
|
const trackingContext = extractNotificationTrackingContext(event.causeMessage);
|
|
174
179
|
const priorityCode = resolvePriorityCauseFromMessage(event.causeMessage);
|
|
175
180
|
if (priorityCode) {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { normalizeNotificationText } from './framework-menu-system-notifications-text';
|
|
4
|
+
|
|
5
|
+
type LegacySkillCause = {
|
|
6
|
+
readonly ruleId: string;
|
|
7
|
+
readonly file?: string;
|
|
8
|
+
readonly line?: string;
|
|
9
|
+
readonly message?: string;
|
|
10
|
+
readonly remediation?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const FIELD_NAMES = ['severity', 'file', 'line', 'lines', 'message', 'remediation'] as const;
|
|
14
|
+
|
|
15
|
+
const RULE_LABELS: Readonly<Record<string, string>> = {
|
|
16
|
+
'skills.ios.prefer-swift-testing': 'Swift Testing',
|
|
17
|
+
'skills.ios.no-wait-for-expectations': 'Swift Testing async',
|
|
18
|
+
'skills.ios.no-xctassert': 'Swift Testing',
|
|
19
|
+
'skills.ios.no-xctunwrap': 'Swift Testing',
|
|
20
|
+
'skills.backend.no-empty-catch': 'Backend: no empty catch',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const extractLegacySkillSegment = (message?: string): string | null => {
|
|
24
|
+
if (!message) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const markerIndex = message.indexOf('Blocking causes:');
|
|
28
|
+
if (markerIndex < 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const afterMarker = message.slice(markerIndex);
|
|
32
|
+
const firstCause = afterMarker.replace(/^Blocking causes:\s*1\)\s*/i, '');
|
|
33
|
+
const nextCauseIndex = firstCause.search(/\s+\d+\)\s+skills\./);
|
|
34
|
+
return nextCauseIndex >= 0 ? firstCause.slice(0, nextCauseIndex) : firstCause;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const extractField = (segment: string, fieldName: string): string | undefined => {
|
|
38
|
+
const nextFields = FIELD_NAMES.filter((name) => name !== fieldName).join('|');
|
|
39
|
+
const pattern = new RegExp(`(?:^|\\s)${fieldName}=([\\s\\S]*?)(?=\\s(?:${nextFields})=|$)`);
|
|
40
|
+
const match = segment.match(pattern);
|
|
41
|
+
const value = match?.[1]?.trim();
|
|
42
|
+
return value && value.length > 0 ? value : undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const humanizeRuleId = (ruleId: string): string =>
|
|
46
|
+
RULE_LABELS[ruleId] ??
|
|
47
|
+
ruleId
|
|
48
|
+
.replace(/^skills\./, '')
|
|
49
|
+
.split(/[._-]+/)
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
52
|
+
.join(' ');
|
|
53
|
+
|
|
54
|
+
const formatLocation = (cause: LegacySkillCause): string | null => {
|
|
55
|
+
if (!cause.file) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const basename = path.basename(cause.file);
|
|
59
|
+
return cause.line ? `${basename}:${cause.line}` : basename;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const extractLegacySkillCauseFromMessage = (
|
|
63
|
+
message?: string
|
|
64
|
+
): LegacySkillCause | null => {
|
|
65
|
+
const segment = extractLegacySkillSegment(message);
|
|
66
|
+
if (!segment) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const ruleId = segment.match(/\bskills\.[a-z0-9_.-]+\b/i)?.[0];
|
|
70
|
+
if (!ruleId) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
ruleId,
|
|
75
|
+
file: extractField(segment, 'file'),
|
|
76
|
+
line: extractField(segment, 'lines') ?? extractField(segment, 'line'),
|
|
77
|
+
message: extractField(segment, 'message'),
|
|
78
|
+
remediation: extractField(segment, 'remediation'),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const buildLegacySkillCauseSummary = (message?: string): string | null => {
|
|
83
|
+
const cause = extractLegacySkillCauseFromMessage(message);
|
|
84
|
+
if (!cause) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const rule = humanizeRuleId(cause.ruleId);
|
|
88
|
+
const location = formatLocation(cause);
|
|
89
|
+
return location ? `Skill violada: ${location} · ${rule}` : `Skill violada: ${rule}`;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const buildLegacySkillCauseRemediation = (message?: string): string | null => {
|
|
93
|
+
const cause = extractLegacySkillCauseFromMessage(message);
|
|
94
|
+
if (!cause) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const rule = humanizeRuleId(cause.ruleId);
|
|
98
|
+
const location = formatLocation(cause);
|
|
99
|
+
const failure = cause.message
|
|
100
|
+
? ` Falla: ${normalizeNotificationText(cause.message)}.`
|
|
101
|
+
: '';
|
|
102
|
+
const remediation = cause.remediation
|
|
103
|
+
? normalizeNotificationText(cause.remediation)
|
|
104
|
+
: 'Corrige esa regla en el fichero afectado y vuelve a intentar el commit.';
|
|
105
|
+
const file = location ? ` Fichero: ${location}.` : '';
|
|
106
|
+
return `Regla: ${rule}.${file}${failure} Solución: ${remediation}`;
|
|
107
|
+
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
extractNotificationTrackingContext,
|
|
9
9
|
TRACKING_BLOCKED_REMEDIATION,
|
|
10
10
|
} from './framework-menu-system-notifications-tracking';
|
|
11
|
+
import { buildLegacySkillCauseRemediation } from './framework-menu-system-notifications-legacy-skill-cause';
|
|
11
12
|
|
|
12
13
|
type BlockedRemediationVariant = 'banner' | 'dialog';
|
|
13
14
|
|
|
@@ -224,6 +225,10 @@ export const resolveBlockedRemediation = (
|
|
|
224
225
|
if (trackingContext) {
|
|
225
226
|
return truncateNotificationText(buildNotificationTrackingRemediation(trackingContext), maxLength);
|
|
226
227
|
}
|
|
228
|
+
const legacySkillCauseRemediation = buildLegacySkillCauseRemediation(event.causeMessage);
|
|
229
|
+
if (legacySkillCauseRemediation) {
|
|
230
|
+
return truncateNotificationText(legacySkillCauseRemediation, maxLength);
|
|
231
|
+
}
|
|
227
232
|
if (fromEvent.length > 0) {
|
|
228
233
|
if (
|
|
229
234
|
causeCode === 'EVIDENCE_GATE_BLOCKED' &&
|