@dailephd/my-dev-kit-lab 0.3.4 → 0.4.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/README.md +46 -270
- package/dist/scripts/security/validate.js +177 -70
- package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +5 -2
- package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +5 -2
- package/dist/src/mobile/android/audit/androidFinding.js +68 -0
- package/dist/src/mobile/android/audit/checkResultBuilder.js +97 -0
- package/dist/src/mobile/android/audit/deepLinkAudit.js +183 -0
- package/dist/src/mobile/android/audit/exportedComponentAudit.js +183 -0
- package/dist/src/mobile/android/audit/intentFilterAudit.js +128 -0
- package/dist/src/mobile/android/audit/permissionAudit.js +142 -0
- package/dist/src/mobile/android/detect/androidTargetDetection.js +18 -0
- package/dist/src/mobile/android/detect/buildFileEvidence.js +36 -0
- package/dist/src/mobile/android/detect/classify.js +150 -0
- package/dist/src/mobile/android/detect/detectAndroidProject.js +252 -0
- package/dist/src/mobile/android/detect/settingsModules.js +60 -0
- package/dist/src/mobile/android/detect/sourceEvidence.js +92 -0
- package/dist/src/mobile/android/detect/traversal.js +79 -0
- package/dist/src/mobile/android/detect/versionCatalogEvidence.js +37 -0
- package/dist/src/mobile/android/detection.js +28 -0
- package/dist/src/mobile/android/gradle/gradleMetadataCheckResult.js +120 -0
- package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +137 -0
- package/dist/src/mobile/android/gradle/pluginVersionExtractor.js +40 -0
- package/dist/src/mobile/android/gradle/readAndroidGradleMetadata.js +210 -0
- package/dist/src/mobile/android/gradle/releaseMetadataSummary.js +64 -0
- package/dist/src/mobile/android/gradle/settingsMetadata.js +20 -0
- package/dist/src/mobile/android/gradle/types.js +1 -0
- package/dist/src/mobile/android/gradle/validate/executor.js +9 -0
- package/dist/src/mobile/android/gradle/validate/operationCheckResult.js +99 -0
- package/dist/src/mobile/android/gradle/validate/operations.js +68 -0
- package/dist/src/mobile/android/gradle/validate/planner.js +49 -0
- package/dist/src/mobile/android/gradle/validate/runOptionalGradleValidation.js +113 -0
- package/dist/src/mobile/android/gradle/validate/targetMutation.js +45 -0
- package/dist/src/mobile/android/gradle/validate/taskListParser.js +24 -0
- package/dist/src/mobile/android/gradle/versionCatalogMetadata.js +94 -0
- package/dist/src/mobile/android/gradle/wrapperMetadata.js +32 -0
- package/dist/src/mobile/android/manifest/parseAndroidManifest.js +341 -0
- package/dist/src/mobile/android/manifest/types.js +29 -0
- package/dist/src/mobile/android/manifest/xml/parseXml.js +313 -0
- package/dist/src/mobile/android/profile.js +29 -0
- package/dist/src/mobile/android/report/model.js +104 -0
- package/dist/src/mobile/android/report/renderAndroidReport.js +223 -0
- package/dist/src/mobile/android/report/writeAndroidReportFiles.js +23 -0
- package/dist/src/mobile/android/targetMetadata.js +27 -0
- package/dist/src/mobile/android/validate/androidVerdict.js +248 -0
- package/dist/src/mobile/android/validate/detectionAndImmutabilityCheckResults.js +177 -0
- package/dist/src/mobile/android/validate/playReadinessChecklist.js +145 -0
- package/dist/src/mobile/android/validate/validateAndroidTarget.js +172 -0
- package/dist/src/mobile/android/validation/checkResult.js +51 -0
- package/dist/src/mobile/android/validation/result.js +25 -0
- package/dist/src/mobile/index.js +42 -0
- package/dist/src/mobile/types.js +27 -0
- package/dist/src/securityValidation/attackScenarios/attackProfile.js +2 -0
- package/dist/src/securityValidation/attackScenarios/profiles/androidProfile.js +20 -0
- package/dist/src/securityValidation/validate/cliOptions.js +46 -2
- package/package.json +2 -1
- package/tests/fixtures/android/compose-app/app/build.gradle.kts +21 -0
- package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +22 -0
- package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +11 -0
- package/tests/fixtures/android/compose-app/build.gradle.kts +4 -0
- package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/compose-app/settings.gradle.kts +2 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +3 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +5 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +21 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +17 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +23 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +4 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +3 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +5 -0
- package/tests/fixtures/android/java-app/app/build.gradle +16 -0
- package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +19 -0
- package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +4 -0
- package/tests/fixtures/android/java-app/build.gradle +3 -0
- package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/java-app/settings.gradle +2 -0
- package/tests/fixtures/android/library/build.gradle.kts +3 -0
- package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/library/mylibrary/build.gradle.kts +13 -0
- package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/library/settings.gradle.kts +2 -0
- package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +14 -0
- package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +10 -0
- package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +5 -0
- package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +7 -0
- package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +11 -0
- package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +29 -0
- package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +62 -0
- package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +45 -0
- package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +14 -0
- package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +11 -0
- package/tests/fixtures/android/multi-module/app/build.gradle.kts +22 -0
- package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +3 -0
- package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +11 -0
- package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +3 -0
- package/tests/fixtures/android/multi-module/build.gradle.kts +5 -0
- package/tests/fixtures/android/multi-module/core/build.gradle.kts +14 -0
- package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +18 -0
- package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +8 -0
- package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/multi-module/settings.gradle.kts +4 -0
- package/tests/fixtures/android/non-android-gradle/build.gradle.kts +3 -0
- package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +3 -0
- package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +4 -0
- package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +2 -0
- package/tests/fixtures/android/partial/app/build.gradle.kts +7 -0
- package/tests/fixtures/android/partial/settings.gradle.kts +2 -0
- package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +17 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +31 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +7 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +4 -0
- package/tests/fixtures/android/xml-view-app/build.gradle.kts +4 -0
- package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/xml-view-app/settings.gradle.kts +2 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { makeAndroidFinding } from "./androidFinding.js";
|
|
2
|
+
import { buildAndroidManifestCheckResult } from "./checkResultBuilder.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.0 Batch 3 — initial intent-filter audit.
|
|
5
|
+
//
|
|
6
|
+
// Distinct lens from the exported-component audit (agents.txt Batch 3
|
|
7
|
+
// section 7.15): this audit does not repeat "is this exported and
|
|
8
|
+
// unprotected" — it looks at the intent-filter *contents* themselves:
|
|
9
|
+
// duplicate/malformed filter data, and custom (non-platform) actions on a
|
|
10
|
+
// component that is reachable (explicitly exported, or unspecified-exported
|
|
11
|
+
// with a filter). A standard MAIN/LAUNCHER filter is never flagged — that is
|
|
12
|
+
// the required negative control (ANDROID-B3-22).
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const STANDARD_ACTION_PREFIX = "android.intent.action.";
|
|
15
|
+
const STANDARD_CATEGORY_PREFIX = "android.intent.category.";
|
|
16
|
+
function isReachable(component) {
|
|
17
|
+
return component.exported === true || (component.exported === undefined && component.exportedRaw === undefined && component.intentFilters.length > 0);
|
|
18
|
+
}
|
|
19
|
+
function hasDuplicates(values) {
|
|
20
|
+
return new Set(values).size !== values.length;
|
|
21
|
+
}
|
|
22
|
+
function auditFilter(entry, kind, component, filter, findings) {
|
|
23
|
+
const identity = component.name || "(unnamed component)";
|
|
24
|
+
const actions = filter.actions ?? [];
|
|
25
|
+
const categories = filter.categories ?? [];
|
|
26
|
+
const dataElements = filter.dataElements ?? filter.filterData;
|
|
27
|
+
if (actions.length === 0 && dataElements.length === 0) {
|
|
28
|
+
findings.push(makeAndroidFinding({
|
|
29
|
+
ruleId: "android-intent-filter-empty-or-malformed",
|
|
30
|
+
title: `Empty or malformed <intent-filter> on ${kind} "${identity}"`,
|
|
31
|
+
severity: "informational",
|
|
32
|
+
confidence: "medium",
|
|
33
|
+
description: "This <intent-filter> declares no actions and no data elements, so it cannot match any implicit intent.",
|
|
34
|
+
manifestPath: entry.manifestPath,
|
|
35
|
+
identity,
|
|
36
|
+
location: filter.location,
|
|
37
|
+
evidenceDetails: [`kind=${kind}`],
|
|
38
|
+
}));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (hasDuplicates(actions) || hasDuplicates(categories)) {
|
|
42
|
+
findings.push(makeAndroidFinding({
|
|
43
|
+
ruleId: "android-intent-filter-duplicate-entries",
|
|
44
|
+
title: `Duplicate action/category entries in an <intent-filter> on ${kind} "${identity}"`,
|
|
45
|
+
severity: "informational",
|
|
46
|
+
confidence: "high",
|
|
47
|
+
description: "This <intent-filter> declares the same <action> or <category> more than once, which has no additional effect and may indicate a copy/paste mistake.",
|
|
48
|
+
manifestPath: entry.manifestPath,
|
|
49
|
+
identity,
|
|
50
|
+
location: filter.location,
|
|
51
|
+
evidenceDetails: [`actions=${actions.join(",")}`, `categories=${categories.join(",")}`],
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
const isStandardLauncherFilter = actions.length === 1 &&
|
|
55
|
+
actions[0] === "android.intent.action.MAIN" &&
|
|
56
|
+
categories.every((c) => c === "android.intent.category.LAUNCHER" || c === "android.intent.category.DEFAULT");
|
|
57
|
+
if (isStandardLauncherFilter) {
|
|
58
|
+
// Negative control: a standard launcher entry point is expected and
|
|
59
|
+
// must not itself be reported as a finding (ANDROID-B3-22).
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const customActions = actions.filter((action) => !action.startsWith(STANDARD_ACTION_PREFIX));
|
|
63
|
+
if (customActions.length > 0 && isReachable(component)) {
|
|
64
|
+
findings.push(makeAndroidFinding({
|
|
65
|
+
ruleId: "android-intent-filter-custom-action-reachable",
|
|
66
|
+
title: `Custom action on a potentially reachable ${kind} "${identity}"`,
|
|
67
|
+
severity: "minor",
|
|
68
|
+
confidence: component.exported === true ? "high" : "medium",
|
|
69
|
+
description: `This <intent-filter> declares a custom (non-platform) action (${customActions.join(", ")}) on a component whose exported state is ${component.exported === true ? "explicitly true" : "unspecified with an intent filter present"}. Custom actions on reachable components broaden the app's externally triggerable surface and are worth reviewing, though the action name alone does not prove exploitability.`,
|
|
70
|
+
manifestPath: entry.manifestPath,
|
|
71
|
+
identity,
|
|
72
|
+
location: filter.location,
|
|
73
|
+
evidenceDetails: [`customActions=${customActions.join(",")}`, `exported=${component.exported ?? "unspecified"}`, `hasComponentPermission=${Boolean(component.permission)}`],
|
|
74
|
+
recommendation: "Confirm this custom action needs to be externally reachable, and add permission protection if not.",
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
const unrecognizedCategories = categories.filter((c) => !c.startsWith(STANDARD_CATEGORY_PREFIX));
|
|
78
|
+
if (unrecognizedCategories.length > 0) {
|
|
79
|
+
findings.push(makeAndroidFinding({
|
|
80
|
+
ruleId: "android-intent-filter-non-standard-category",
|
|
81
|
+
title: `Non-standard intent-filter category on ${kind} "${identity}"`,
|
|
82
|
+
severity: "informational",
|
|
83
|
+
confidence: "medium",
|
|
84
|
+
description: `This <intent-filter> declares a category outside the standard android.intent.category.* namespace (${unrecognizedCategories.join(", ")}).`,
|
|
85
|
+
manifestPath: entry.manifestPath,
|
|
86
|
+
identity,
|
|
87
|
+
location: filter.location,
|
|
88
|
+
evidenceDetails: [`categories=${unrecognizedCategories.join(",")}`],
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function componentGroups(manifest) {
|
|
93
|
+
return [
|
|
94
|
+
{ kind: "activity", components: manifest.activities },
|
|
95
|
+
{ kind: "activity-alias", components: manifest.activityAliases ?? [] },
|
|
96
|
+
{ kind: "service", components: manifest.services },
|
|
97
|
+
{ kind: "receiver", components: manifest.receivers },
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
export function auditAndroidIntentFilters(detection, manifests) {
|
|
101
|
+
const findings = [];
|
|
102
|
+
const evidence = [];
|
|
103
|
+
const warnings = [];
|
|
104
|
+
for (const entry of manifests) {
|
|
105
|
+
let filterCount = 0;
|
|
106
|
+
for (const group of componentGroups(entry.manifest)) {
|
|
107
|
+
for (const component of group.components) {
|
|
108
|
+
for (const filter of component.intentFilters) {
|
|
109
|
+
filterCount += 1;
|
|
110
|
+
auditFilter(entry, group.kind, component, filter, findings);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (filterCount > 0) {
|
|
115
|
+
evidence.push(`${entry.manifestPath}: ${filterCount} intent-filter(s) inspected`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return buildAndroidManifestCheckResult({
|
|
119
|
+
id: "android-intent-filters-audit",
|
|
120
|
+
category: "android-intent-filters",
|
|
121
|
+
title: "Android intent-filter audit",
|
|
122
|
+
detection,
|
|
123
|
+
manifests,
|
|
124
|
+
findings,
|
|
125
|
+
evidence,
|
|
126
|
+
warnings,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { makeAndroidFinding } from "./androidFinding.js";
|
|
2
|
+
import { buildAndroidManifestCheckResult } from "./checkResultBuilder.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.0 Batch 3 — initial Android permission audit.
|
|
5
|
+
//
|
|
6
|
+
// Conservative by design (agents.txt Batch 3 section 7.13): a declared
|
|
7
|
+
// permission is not automatically a vulnerability. Ordinary connectivity
|
|
8
|
+
// permissions (INTERNET etc.) never produce a finding. Recognized sensitive
|
|
9
|
+
// permission groups produce a "minor" (review-required) finding. Unknown/
|
|
10
|
+
// custom permissions produce an "informational" review finding. The only
|
|
11
|
+
// escalation is foreground + background location declared together, which
|
|
12
|
+
// still stays at "minor" (a legitimate, common pattern — not proof of
|
|
13
|
+
// misuse).
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const LOW_RISK_PERMISSIONS = new Set([
|
|
16
|
+
"android.permission.INTERNET",
|
|
17
|
+
"android.permission.ACCESS_NETWORK_STATE",
|
|
18
|
+
"android.permission.ACCESS_WIFI_STATE",
|
|
19
|
+
"android.permission.VIBRATE",
|
|
20
|
+
"android.permission.WAKE_LOCK",
|
|
21
|
+
"android.permission.RECEIVE_BOOT_COMPLETED",
|
|
22
|
+
]);
|
|
23
|
+
const SENSITIVE_PERMISSION_GROUPS = {
|
|
24
|
+
"android.permission.ACCESS_FINE_LOCATION": "location",
|
|
25
|
+
"android.permission.ACCESS_COARSE_LOCATION": "location",
|
|
26
|
+
"android.permission.ACCESS_BACKGROUND_LOCATION": "background-location",
|
|
27
|
+
"android.permission.CAMERA": "camera",
|
|
28
|
+
"android.permission.RECORD_AUDIO": "microphone",
|
|
29
|
+
"android.permission.READ_CONTACTS": "contacts",
|
|
30
|
+
"android.permission.WRITE_CONTACTS": "contacts",
|
|
31
|
+
"android.permission.GET_ACCOUNTS": "contacts",
|
|
32
|
+
"android.permission.READ_CALENDAR": "calendar",
|
|
33
|
+
"android.permission.WRITE_CALENDAR": "calendar",
|
|
34
|
+
"android.permission.SEND_SMS": "sms",
|
|
35
|
+
"android.permission.READ_SMS": "sms",
|
|
36
|
+
"android.permission.RECEIVE_SMS": "sms",
|
|
37
|
+
"android.permission.RECEIVE_MMS": "sms",
|
|
38
|
+
"android.permission.READ_CALL_LOG": "call-log",
|
|
39
|
+
"android.permission.WRITE_CALL_LOG": "call-log",
|
|
40
|
+
"android.permission.PROCESS_OUTGOING_CALLS": "call-log",
|
|
41
|
+
"android.permission.READ_PHONE_STATE": "phone",
|
|
42
|
+
"android.permission.CALL_PHONE": "phone",
|
|
43
|
+
"android.permission.BLUETOOTH_CONNECT": "nearby-devices",
|
|
44
|
+
"android.permission.BLUETOOTH_SCAN": "nearby-devices",
|
|
45
|
+
"android.permission.BLUETOOTH_ADVERTISE": "nearby-devices",
|
|
46
|
+
"android.permission.NEARBY_WIFI_DEVICES": "nearby-devices",
|
|
47
|
+
"android.permission.READ_EXTERNAL_STORAGE": "storage-legacy",
|
|
48
|
+
"android.permission.WRITE_EXTERNAL_STORAGE": "storage-legacy",
|
|
49
|
+
"android.permission.MANAGE_EXTERNAL_STORAGE": "storage-legacy",
|
|
50
|
+
"android.permission.READ_MEDIA_IMAGES": "media",
|
|
51
|
+
"android.permission.READ_MEDIA_VIDEO": "media",
|
|
52
|
+
"android.permission.READ_MEDIA_AUDIO": "media",
|
|
53
|
+
"android.permission.POST_NOTIFICATIONS": "notifications",
|
|
54
|
+
"android.permission.BODY_SENSORS": "body-sensors",
|
|
55
|
+
"android.permission.BODY_SENSORS_BACKGROUND": "body-sensors",
|
|
56
|
+
"android.permission.ACTIVITY_RECOGNITION": "activity-recognition",
|
|
57
|
+
"android.permission.FOREGROUND_SERVICE": "foreground-service",
|
|
58
|
+
"android.permission.REQUEST_INSTALL_PACKAGES": "package-install",
|
|
59
|
+
"android.permission.QUERY_ALL_PACKAGES": "package-visibility",
|
|
60
|
+
};
|
|
61
|
+
export function auditAndroidPermissions(detection, manifests) {
|
|
62
|
+
const findings = [];
|
|
63
|
+
const evidence = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
for (const entry of manifests) {
|
|
66
|
+
const declaredNames = new Set(entry.manifest.permissions.map((p) => p.name));
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const permission of entry.manifest.permissions) {
|
|
69
|
+
if (seen.has(permission.name))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(permission.name);
|
|
72
|
+
if (LOW_RISK_PERMISSIONS.has(permission.name)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const group = SENSITIVE_PERMISSION_GROUPS[permission.name];
|
|
76
|
+
const duplicateCount = entry.manifest.permissions.filter((p) => p.name === permission.name).length;
|
|
77
|
+
const duplicateNote = duplicateCount > 1 ? [`declared ${duplicateCount} times (deduplicated for this finding)`] : [];
|
|
78
|
+
if (group === "background-location") {
|
|
79
|
+
const hasForeground = declaredNames.has("android.permission.ACCESS_FINE_LOCATION") || declaredNames.has("android.permission.ACCESS_COARSE_LOCATION");
|
|
80
|
+
findings.push(makeAndroidFinding({
|
|
81
|
+
ruleId: hasForeground ? "android-permission-background-and-foreground-location" : "android-permission-sensitive",
|
|
82
|
+
title: hasForeground
|
|
83
|
+
? "Background location declared alongside foreground location"
|
|
84
|
+
: `Sensitive permission declared: ${permission.name}`,
|
|
85
|
+
severity: "minor",
|
|
86
|
+
confidence: "high",
|
|
87
|
+
description: hasForeground
|
|
88
|
+
? "The manifest declares ACCESS_BACKGROUND_LOCATION together with a foreground location permission. This is a common, legitimate pattern, but background location access typically requires explicit justification for store review."
|
|
89
|
+
: `The manifest declares the sensitive permission ${permission.name} (group: ${group}). This is not automatically a vulnerability; review that the app's functionality justifies the request.`,
|
|
90
|
+
manifestPath: entry.manifestPath,
|
|
91
|
+
identity: permission.name,
|
|
92
|
+
location: permission.location,
|
|
93
|
+
evidenceDetails: [`sourceElement=${permission.sourceElement ?? "uses-permission"}`, `group=${group}`, ...duplicateNote],
|
|
94
|
+
recommendation: "Confirm the permission is required for a real user-facing feature and document the justification.",
|
|
95
|
+
}));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (group) {
|
|
99
|
+
findings.push(makeAndroidFinding({
|
|
100
|
+
ruleId: "android-permission-sensitive",
|
|
101
|
+
title: `Sensitive permission declared: ${permission.name}`,
|
|
102
|
+
severity: "minor",
|
|
103
|
+
confidence: "high",
|
|
104
|
+
description: `The manifest declares the sensitive permission ${permission.name} (group: ${group}). This is not automatically a vulnerability; review that the app's functionality justifies the request.`,
|
|
105
|
+
manifestPath: entry.manifestPath,
|
|
106
|
+
identity: permission.name,
|
|
107
|
+
location: permission.location,
|
|
108
|
+
evidenceDetails: [`sourceElement=${permission.sourceElement ?? "uses-permission"}`, `group=${group}`, ...duplicateNote],
|
|
109
|
+
recommendation: "Confirm the permission is required for a real user-facing feature and document the justification.",
|
|
110
|
+
}));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!permission.name.startsWith("android.permission.")) {
|
|
114
|
+
findings.push(makeAndroidFinding({
|
|
115
|
+
ruleId: "android-permission-unknown-custom",
|
|
116
|
+
title: `Unknown custom permission declared: ${permission.name}`,
|
|
117
|
+
severity: "informational",
|
|
118
|
+
confidence: "high",
|
|
119
|
+
description: `The manifest declares a custom permission "${permission.name}" that is not a recognized platform permission. This is not automatically dangerous; review its purpose and, if it protects app components, whether its protectionLevel is appropriate.`,
|
|
120
|
+
manifestPath: entry.manifestPath,
|
|
121
|
+
identity: permission.name,
|
|
122
|
+
location: permission.location,
|
|
123
|
+
evidenceDetails: [`sourceElement=${permission.sourceElement ?? "uses-permission"}`, ...duplicateNote],
|
|
124
|
+
recommendation: "Review the custom permission's purpose and protection level.",
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (entry.manifest.permissions.length > 0) {
|
|
129
|
+
evidence.push(`${entry.manifestPath}: ${entry.manifest.permissions.length} permission declaration(s) inspected`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return buildAndroidManifestCheckResult({
|
|
133
|
+
id: "android-permissions-audit",
|
|
134
|
+
category: "android-permissions",
|
|
135
|
+
title: "Android permission audit",
|
|
136
|
+
detection,
|
|
137
|
+
manifests,
|
|
138
|
+
findings,
|
|
139
|
+
evidence,
|
|
140
|
+
warnings,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createAndroidProfile } from "../profile.js";
|
|
2
|
+
import { createAndroidTargetMetadata } from "../targetMetadata.js";
|
|
3
|
+
import { detectAndroidProject } from "./detectAndroidProject.js";
|
|
4
|
+
export function detectAndroidTarget(local) {
|
|
5
|
+
const detection = detectAndroidProject(local.targetRoot);
|
|
6
|
+
const androidProfile = createAndroidProfile({
|
|
7
|
+
detectionConfidence: detection.confidence,
|
|
8
|
+
evidence: detection.evidence,
|
|
9
|
+
unsupportedNotes: detection.warnings,
|
|
10
|
+
projectType: detection.projectKind,
|
|
11
|
+
projectSubtype: detection.uiToolkit,
|
|
12
|
+
supportedCapabilities: ["static-detection"],
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
detection,
|
|
16
|
+
target: createAndroidTargetMetadata({ local, androidProfile, detection }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.0 Batch 2 — bounded, deterministic build.gradle(.kts) evidence
|
|
3
|
+
// extraction.
|
|
4
|
+
//
|
|
5
|
+
// Extracts only the lexical signals needed for detection and classification
|
|
6
|
+
// (plugin ids, namespace, applicationId, Compose build-feature/dependency
|
|
7
|
+
// evidence). This is not a Gradle metadata parser (that is Batch 4's scope):
|
|
8
|
+
// values that cannot be statically read as a literal are preserved as
|
|
9
|
+
// "unresolved" evidence rather than guessed.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const ANDROID_APPLICATION_PLUGIN_PATTERN = /(['"`])com\.android\.application\1|id\s*\(\s*['"`]com\.android\.application['"`]\s*\)/;
|
|
12
|
+
const ANDROID_LIBRARY_PLUGIN_PATTERN = /(['"`])com\.android\.library\1|id\s*\(\s*['"`]com\.android\.library['"`]\s*\)/;
|
|
13
|
+
const KOTLIN_ANDROID_PLUGIN_PATTERN = /(['"`])org\.jetbrains\.kotlin\.android\1|kotlin\s*\(\s*['"`]android['"`]\s*\)|(['"`])kotlin-android\2/;
|
|
14
|
+
const NAMESPACE_LITERAL_PATTERN = /\bnamespace\s*[=]?\s*['"]([^'"]+)['"]/;
|
|
15
|
+
const NAMESPACE_UNRESOLVED_PATTERN = /\bnamespace\s*[=]?\s*(?!['"])[^\s\n]/;
|
|
16
|
+
const APPLICATION_ID_LITERAL_PATTERN = /\bapplicationId\s*[=]?\s*['"]([^'"]+)['"]/;
|
|
17
|
+
const APPLICATION_ID_UNRESOLVED_PATTERN = /\bapplicationId\s*[=]?\s*(?!['"])[^\s\n]/;
|
|
18
|
+
const COMPOSE_BUILD_FEATURE_PATTERN = /\bcompose\s*[=]?\s*true\b/;
|
|
19
|
+
const COMPOSE_DEPENDENCY_PATTERN = /androidx\.compose|compose-bom|compose\.(?:ui|material|material3|runtime|foundation)/;
|
|
20
|
+
const COMPOSE_PLUGIN_PATTERN = /org\.jetbrains\.kotlin\.plugin\.compose|com\.android\.compose|compose-compiler/;
|
|
21
|
+
export function extractBuildFileEvidence(text) {
|
|
22
|
+
const namespaceLiteral = text.match(NAMESPACE_LITERAL_PATTERN);
|
|
23
|
+
const applicationIdLiteral = text.match(APPLICATION_ID_LITERAL_PATTERN);
|
|
24
|
+
return {
|
|
25
|
+
androidApplicationPlugin: ANDROID_APPLICATION_PLUGIN_PATTERN.test(text),
|
|
26
|
+
androidLibraryPlugin: ANDROID_LIBRARY_PLUGIN_PATTERN.test(text),
|
|
27
|
+
kotlinAndroidPlugin: KOTLIN_ANDROID_PLUGIN_PATTERN.test(text),
|
|
28
|
+
namespace: namespaceLiteral?.[1],
|
|
29
|
+
namespaceUnresolved: !namespaceLiteral && NAMESPACE_UNRESOLVED_PATTERN.test(text),
|
|
30
|
+
applicationId: applicationIdLiteral?.[1],
|
|
31
|
+
applicationIdUnresolved: !applicationIdLiteral && APPLICATION_ID_UNRESOLVED_PATTERN.test(text),
|
|
32
|
+
composeBuildFeatureEvidence: COMPOSE_BUILD_FEATURE_PATTERN.test(text),
|
|
33
|
+
composeDependencyEvidence: COMPOSE_DEPENDENCY_PATTERN.test(text),
|
|
34
|
+
composePluginEvidence: COMPOSE_PLUGIN_PATTERN.test(text),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.0 Batch 2 — deterministic, evidence-based classification policy.
|
|
3
|
+
//
|
|
4
|
+
// Pure functions only (no filesystem access) so the confidence/classification
|
|
5
|
+
// policy can be unit tested independently of traversal. See agents.txt Batch
|
|
6
|
+
// 2 section 7.4 for the evidence-strength tiers this implements:
|
|
7
|
+
// - Strong: an Android plugin id, or namespace/applicationId combined with
|
|
8
|
+
// an Android plugin, or a manifest inside an Android-style source set.
|
|
9
|
+
// - Supporting: source roots, res/, androidTest, wrapper, settings files,
|
|
10
|
+
// catalog aliases, Compose dependencies.
|
|
11
|
+
// - Weak: generic Gradle/Kotlin/Java files or directory names alone.
|
|
12
|
+
// A project must never be confirmed Android from weak evidence alone.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const CONFIDENCE_RANK = { unknown: 0, low: 1, medium: 2, high: 3 };
|
|
15
|
+
export function maxConfidence(a, b) {
|
|
16
|
+
return CONFIDENCE_RANK[a] >= CONFIDENCE_RANK[b] ? a : b;
|
|
17
|
+
}
|
|
18
|
+
function capConfidence(confidence, cap) {
|
|
19
|
+
return CONFIDENCE_RANK[confidence] <= CONFIDENCE_RANK[cap] ? confidence : cap;
|
|
20
|
+
}
|
|
21
|
+
// Classifies a single Gradle module from its collected static evidence.
|
|
22
|
+
// Directory naming (e.g. a module literally called "app") is never used as a
|
|
23
|
+
// signal — only plugin declarations, manifest presence, and Android-specific
|
|
24
|
+
// identifiers.
|
|
25
|
+
export function classifyModule(input) {
|
|
26
|
+
const evidence = [];
|
|
27
|
+
if (input.hasAndroidApplicationPlugin) {
|
|
28
|
+
evidence.push("com.android.application plugin declared");
|
|
29
|
+
const strongCombo = input.hasManifest || Boolean(input.namespace) || Boolean(input.applicationId);
|
|
30
|
+
if (input.hasManifest)
|
|
31
|
+
evidence.push("AndroidManifest.xml present in module source set");
|
|
32
|
+
if (input.namespace)
|
|
33
|
+
evidence.push(`namespace = "${input.namespace}"`);
|
|
34
|
+
if (input.applicationId)
|
|
35
|
+
evidence.push(`applicationId = "${input.applicationId}"`);
|
|
36
|
+
return { kind: "application", confidence: strongCombo ? "high" : "medium", evidence };
|
|
37
|
+
}
|
|
38
|
+
if (input.hasAndroidLibraryPlugin) {
|
|
39
|
+
evidence.push("com.android.library plugin declared");
|
|
40
|
+
const strongCombo = input.hasManifest || Boolean(input.namespace);
|
|
41
|
+
if (input.hasManifest)
|
|
42
|
+
evidence.push("AndroidManifest.xml present in module source set");
|
|
43
|
+
if (input.namespace)
|
|
44
|
+
evidence.push(`namespace = "${input.namespace}"`);
|
|
45
|
+
return { kind: "library", confidence: strongCombo ? "high" : "medium", evidence };
|
|
46
|
+
}
|
|
47
|
+
if (input.hasCatalogAndroidPluginAliasEvidence) {
|
|
48
|
+
evidence.push("version-catalog plugin alias referencing an Android Gradle Plugin entry");
|
|
49
|
+
return { kind: "unknown", confidence: "medium", evidence };
|
|
50
|
+
}
|
|
51
|
+
// Generic Java/Kotlin source roots are deliberately excluded from this
|
|
52
|
+
// list: they exist in any JVM project and are not Android-specific, so
|
|
53
|
+
// they must never by themselves push a module toward Android detection
|
|
54
|
+
// (agents.txt Batch 2 section 12.4 — generic Gradle/Kotlin/Java evidence
|
|
55
|
+
// must not trigger Android detection without Android-specific evidence).
|
|
56
|
+
const weakSignals = [];
|
|
57
|
+
if (input.hasManifest)
|
|
58
|
+
weakSignals.push("AndroidManifest.xml present without a resolved Android plugin");
|
|
59
|
+
if (input.namespace)
|
|
60
|
+
weakSignals.push(`namespace = "${input.namespace}" without a resolved Android plugin`);
|
|
61
|
+
if (input.applicationId)
|
|
62
|
+
weakSignals.push(`applicationId = "${input.applicationId}" without a resolved Android plugin`);
|
|
63
|
+
if (input.hasResDir)
|
|
64
|
+
weakSignals.push("res/ directory present");
|
|
65
|
+
if (weakSignals.length > 0) {
|
|
66
|
+
evidence.push(...weakSignals);
|
|
67
|
+
return { kind: "unknown", confidence: "low", evidence };
|
|
68
|
+
}
|
|
69
|
+
if (input.hasGenericBuildFile) {
|
|
70
|
+
evidence.push("Gradle build file present with no Android-specific evidence");
|
|
71
|
+
return { kind: "non-android", confidence: "medium", evidence };
|
|
72
|
+
}
|
|
73
|
+
return { kind: "unknown", confidence: "unknown", evidence: [] };
|
|
74
|
+
}
|
|
75
|
+
// Classifies UI toolkit usage for a single module. Conservative by design:
|
|
76
|
+
// Kotlin usage alone never implies Compose, and res/ alone never implies
|
|
77
|
+
// XML/View classification beyond what's asserted here (a literal layout
|
|
78
|
+
// resource file, not merely a res/ directory).
|
|
79
|
+
export function classifyModuleUiToolkit(input) {
|
|
80
|
+
const evidence = [];
|
|
81
|
+
const strongCompose = input.composeBuildFeatureEvidence || input.composeDependencyEvidence || input.composeSourceEvidence;
|
|
82
|
+
const strongView = input.hasResLayout;
|
|
83
|
+
if (input.composeBuildFeatureEvidence)
|
|
84
|
+
evidence.push("buildFeatures { compose = true }");
|
|
85
|
+
if (input.composeDependencyEvidence)
|
|
86
|
+
evidence.push("Compose dependency evidence in build file");
|
|
87
|
+
if (input.composeSourceEvidence)
|
|
88
|
+
evidence.push("@Composable/androidx.compose import evidence in source");
|
|
89
|
+
if (input.composePluginEvidence)
|
|
90
|
+
evidence.push("Compose compiler plugin evidence");
|
|
91
|
+
if (input.hasResLayout)
|
|
92
|
+
evidence.push("res/layout XML resource present");
|
|
93
|
+
if (input.viewSourceEvidence)
|
|
94
|
+
evidence.push("traditional Android View usage evidence in source");
|
|
95
|
+
if (strongCompose && strongView) {
|
|
96
|
+
return { uiToolkit: "mixed", evidence };
|
|
97
|
+
}
|
|
98
|
+
if (strongCompose) {
|
|
99
|
+
return { uiToolkit: "compose", evidence };
|
|
100
|
+
}
|
|
101
|
+
if (strongView) {
|
|
102
|
+
return { uiToolkit: "xml-view", evidence };
|
|
103
|
+
}
|
|
104
|
+
return { uiToolkit: "uncertain", evidence };
|
|
105
|
+
}
|
|
106
|
+
// Combines per-module kinds into the overall project-level classification.
|
|
107
|
+
// Uses the widened ANDROID_PROJECT_KINDS vocabulary (see detection.ts) — a
|
|
108
|
+
// project with both application and library modules is "mixed"; more than
|
|
109
|
+
// one Android module of the same kind is "multi-module"; a single confirmed
|
|
110
|
+
// module keeps its own kind; Android-ish-but-unresolved evidence with no
|
|
111
|
+
// confirmed module is "partial"; nothing at all is "non-android".
|
|
112
|
+
export function combineProjectKind(modules, hasUnresolvedAndroidEvidence) {
|
|
113
|
+
const applicationCount = modules.filter((module) => module.kind === "application").length;
|
|
114
|
+
const libraryCount = modules.filter((module) => module.kind === "library").length;
|
|
115
|
+
const unknownCount = modules.filter((module) => module.kind === "unknown").length;
|
|
116
|
+
if (applicationCount >= 1 && libraryCount >= 1)
|
|
117
|
+
return "mixed";
|
|
118
|
+
if (applicationCount + libraryCount > 1)
|
|
119
|
+
return "multi-module";
|
|
120
|
+
if (applicationCount === 1)
|
|
121
|
+
return "application";
|
|
122
|
+
if (libraryCount === 1)
|
|
123
|
+
return "library";
|
|
124
|
+
if (unknownCount > 0 || hasUnresolvedAndroidEvidence)
|
|
125
|
+
return "partial";
|
|
126
|
+
return "non-android";
|
|
127
|
+
}
|
|
128
|
+
export function combineProjectUiToolkit(modules) {
|
|
129
|
+
const androidModules = modules.filter((module) => module.kind === "application" || module.kind === "library");
|
|
130
|
+
const composeCount = androidModules.filter((module) => module.uiToolkit === "compose").length;
|
|
131
|
+
const viewCount = androidModules.filter((module) => module.uiToolkit === "xml-view").length;
|
|
132
|
+
const mixedCount = androidModules.filter((module) => module.uiToolkit === "mixed").length;
|
|
133
|
+
if (mixedCount > 0 || (composeCount > 0 && viewCount > 0))
|
|
134
|
+
return "mixed";
|
|
135
|
+
if (composeCount > 0)
|
|
136
|
+
return "compose";
|
|
137
|
+
if (viewCount > 0)
|
|
138
|
+
return "xml-view";
|
|
139
|
+
return "uncertain";
|
|
140
|
+
}
|
|
141
|
+
export function combineProjectConfidence(projectKind, moduleConfidences, hasNonAndroidEvidenceOnly) {
|
|
142
|
+
if (projectKind === "non-android") {
|
|
143
|
+
return hasNonAndroidEvidenceOnly ? "high" : "unknown";
|
|
144
|
+
}
|
|
145
|
+
const strongest = moduleConfidences.reduce((acc, confidence) => maxConfidence(acc, confidence), "unknown");
|
|
146
|
+
if (projectKind === "partial") {
|
|
147
|
+
return capConfidence(strongest, "medium");
|
|
148
|
+
}
|
|
149
|
+
return strongest;
|
|
150
|
+
}
|