@dailephd/my-dev-kit-lab 0.3.3 → 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.
Files changed (150) hide show
  1. package/README.md +46 -269
  2. package/dist/scripts/security/validate.js +177 -70
  3. package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +5 -2
  4. package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +5 -2
  5. package/dist/src/audits/core/collectSourceFacts.js +21 -10
  6. package/dist/src/mobile/android/audit/androidFinding.js +68 -0
  7. package/dist/src/mobile/android/audit/checkResultBuilder.js +97 -0
  8. package/dist/src/mobile/android/audit/deepLinkAudit.js +183 -0
  9. package/dist/src/mobile/android/audit/exportedComponentAudit.js +183 -0
  10. package/dist/src/mobile/android/audit/intentFilterAudit.js +128 -0
  11. package/dist/src/mobile/android/audit/permissionAudit.js +142 -0
  12. package/dist/src/mobile/android/detect/androidTargetDetection.js +18 -0
  13. package/dist/src/mobile/android/detect/buildFileEvidence.js +36 -0
  14. package/dist/src/mobile/android/detect/classify.js +150 -0
  15. package/dist/src/mobile/android/detect/detectAndroidProject.js +252 -0
  16. package/dist/src/mobile/android/detect/settingsModules.js +60 -0
  17. package/dist/src/mobile/android/detect/sourceEvidence.js +92 -0
  18. package/dist/src/mobile/android/detect/traversal.js +79 -0
  19. package/dist/src/mobile/android/detect/versionCatalogEvidence.js +37 -0
  20. package/dist/src/mobile/android/detection.js +28 -0
  21. package/dist/src/mobile/android/gradle/gradleMetadataCheckResult.js +120 -0
  22. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +137 -0
  23. package/dist/src/mobile/android/gradle/pluginVersionExtractor.js +40 -0
  24. package/dist/src/mobile/android/gradle/readAndroidGradleMetadata.js +210 -0
  25. package/dist/src/mobile/android/gradle/releaseMetadataSummary.js +64 -0
  26. package/dist/src/mobile/android/gradle/settingsMetadata.js +20 -0
  27. package/dist/src/mobile/android/gradle/types.js +1 -0
  28. package/dist/src/mobile/android/gradle/validate/executor.js +9 -0
  29. package/dist/src/mobile/android/gradle/validate/operationCheckResult.js +99 -0
  30. package/dist/src/mobile/android/gradle/validate/operations.js +68 -0
  31. package/dist/src/mobile/android/gradle/validate/planner.js +49 -0
  32. package/dist/src/mobile/android/gradle/validate/runOptionalGradleValidation.js +113 -0
  33. package/dist/src/mobile/android/gradle/validate/targetMutation.js +45 -0
  34. package/dist/src/mobile/android/gradle/validate/taskListParser.js +24 -0
  35. package/dist/src/mobile/android/gradle/versionCatalogMetadata.js +94 -0
  36. package/dist/src/mobile/android/gradle/wrapperMetadata.js +32 -0
  37. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +341 -0
  38. package/dist/src/mobile/android/manifest/types.js +29 -0
  39. package/dist/src/mobile/android/manifest/xml/parseXml.js +313 -0
  40. package/dist/src/mobile/android/profile.js +29 -0
  41. package/dist/src/mobile/android/report/model.js +104 -0
  42. package/dist/src/mobile/android/report/renderAndroidReport.js +223 -0
  43. package/dist/src/mobile/android/report/writeAndroidReportFiles.js +23 -0
  44. package/dist/src/mobile/android/targetMetadata.js +27 -0
  45. package/dist/src/mobile/android/validate/androidVerdict.js +248 -0
  46. package/dist/src/mobile/android/validate/detectionAndImmutabilityCheckResults.js +177 -0
  47. package/dist/src/mobile/android/validate/playReadinessChecklist.js +145 -0
  48. package/dist/src/mobile/android/validate/validateAndroidTarget.js +172 -0
  49. package/dist/src/mobile/android/validation/checkResult.js +51 -0
  50. package/dist/src/mobile/android/validation/result.js +25 -0
  51. package/dist/src/mobile/index.js +42 -0
  52. package/dist/src/mobile/types.js +27 -0
  53. package/dist/src/securityValidation/attackScenarios/attackProfile.js +2 -0
  54. package/dist/src/securityValidation/attackScenarios/profiles/androidProfile.js +20 -0
  55. package/dist/src/securityValidation/validate/cliOptions.js +46 -2
  56. package/package.json +2 -1
  57. package/tests/fixtures/android/compose-app/app/build.gradle.kts +21 -0
  58. package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +22 -0
  59. package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +11 -0
  60. package/tests/fixtures/android/compose-app/build.gradle.kts +4 -0
  61. package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  62. package/tests/fixtures/android/compose-app/settings.gradle.kts +2 -0
  63. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +3 -0
  64. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +5 -0
  65. package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +21 -0
  66. package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +17 -0
  67. package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +23 -0
  68. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +4 -0
  69. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +3 -0
  70. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +5 -0
  71. package/tests/fixtures/android/java-app/app/build.gradle +16 -0
  72. package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +19 -0
  73. package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +4 -0
  74. package/tests/fixtures/android/java-app/build.gradle +3 -0
  75. package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  76. package/tests/fixtures/android/java-app/settings.gradle +2 -0
  77. package/tests/fixtures/android/library/build.gradle.kts +3 -0
  78. package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +4 -0
  79. package/tests/fixtures/android/library/mylibrary/build.gradle.kts +13 -0
  80. package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +2 -0
  81. package/tests/fixtures/android/library/settings.gradle.kts +2 -0
  82. package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +14 -0
  83. package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +10 -0
  84. package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +5 -0
  85. package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +7 -0
  86. package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +11 -0
  87. package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +29 -0
  88. package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +62 -0
  89. package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +45 -0
  90. package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +14 -0
  91. package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +11 -0
  92. package/tests/fixtures/android/multi-module/app/build.gradle.kts +22 -0
  93. package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +3 -0
  94. package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +11 -0
  95. package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +3 -0
  96. package/tests/fixtures/android/multi-module/build.gradle.kts +5 -0
  97. package/tests/fixtures/android/multi-module/core/build.gradle.kts +14 -0
  98. package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +2 -0
  99. package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +18 -0
  100. package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +2 -0
  101. package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +8 -0
  102. package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +4 -0
  103. package/tests/fixtures/android/multi-module/settings.gradle.kts +4 -0
  104. package/tests/fixtures/android/non-android-gradle/build.gradle.kts +3 -0
  105. package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +3 -0
  106. package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +4 -0
  107. package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +4 -0
  108. package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +2 -0
  109. package/tests/fixtures/android/partial/app/build.gradle.kts +7 -0
  110. package/tests/fixtures/android/partial/settings.gradle.kts +2 -0
  111. package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +17 -0
  112. package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +31 -0
  113. package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +7 -0
  114. package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +4 -0
  115. package/tests/fixtures/android/xml-view-app/build.gradle.kts +4 -0
  116. package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  117. package/tests/fixtures/android/xml-view-app/settings.gradle.kts +2 -0
  118. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/README.md +16 -0
  119. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/build.gradle.kts +4 -0
  120. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/java/com/example/Widget.java +7 -0
  121. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/kotlin/com/example/Gadget.kt +7 -0
  122. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/README.md +10 -0
  123. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/build/Main.class +1 -0
  124. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/dist/index.js +1 -0
  125. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/generated/schema.ts +1 -0
  126. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/src/index.ts +3 -0
  127. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/vendor/lib.py +2 -0
  128. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/README.md +10 -0
  129. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/build.gradle.kts +8 -0
  130. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/settings.gradle.kts +1 -0
  131. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/java/com/example/FooService.java +7 -0
  132. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/kotlin/com/example/BarService.kt +7 -0
  133. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/java/com/example/FooServiceTest.java +8 -0
  134. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/kotlin/com/example/BarServiceTest.kt +8 -0
  135. package/tests/fixtures/audits/mixed-language/mixed-python-ts/README.md +8 -0
  136. package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/app.py +7 -0
  137. package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/index.ts +3 -0
  138. package/tests/fixtures/audits/mixed-language/mixed-python-ts/tests/test_app.py +5 -0
  139. package/tests/fixtures/audits/mixed-language/mixed-ts-js/README.md +7 -0
  140. package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/helper.js +5 -0
  141. package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/index.ts +3 -0
  142. package/tests/fixtures/audits/mixed-language/mixed-ts-js/tests/index.sample.ts +5 -0
  143. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/README.md +10 -0
  144. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/build.gradle.kts +4 -0
  145. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/pytests/test_app.py +5 -0
  146. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/app.py +2 -0
  147. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/helper.js +5 -0
  148. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/index.ts +3 -0
  149. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/java/com/example/Multiplier.java +7 -0
  150. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/kotlin/com/example/Divider.kt +7 -0
@@ -0,0 +1,341 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { relativeWithinRoot, resolveWithinRoot } from "../../../core/pathSafety.js";
4
+ import { ANDROID_NAMESPACE_URI, findChildren, getAttribute, parseXmlDocument, } from "./xml/parseXml.js";
5
+ // ---------------------------------------------------------------------------
6
+ // v0.4.0 Batch 3 — AndroidManifest.xml parser.
7
+ //
8
+ // Maps the bounded XML tree from ./xml/parseXml.ts onto the Batch 1
9
+ // AndroidManifestModel contract. Static, deterministic, read-only: no
10
+ // resource-reference resolution, no manifest-placeholder resolution (values
11
+ // like @string/app_name and ${applicationId} are preserved verbatim), and no
12
+ // manifest merging. Malformed input always produces a valid (possibly
13
+ // mostly-empty) AndroidManifestModel with the problem recorded in
14
+ // `parseWarnings`/`unsupportedConstructs` rather than throwing.
15
+ // ---------------------------------------------------------------------------
16
+ function toLocation(xmlLocation) {
17
+ return { line: xmlLocation.line, column: xmlLocation.column };
18
+ }
19
+ function getUnprefixedAttribute(element, localName) {
20
+ return element.attributes.find((attr) => attr.prefix === undefined && attr.localName === localName)?.value;
21
+ }
22
+ function androidAttr(element, localName) {
23
+ return getAttribute(element, localName, ANDROID_NAMESPACE_URI)?.value;
24
+ }
25
+ function parseBooleanAttr(element, localName, warnings, context) {
26
+ const attr = getAttribute(element, localName, ANDROID_NAMESPACE_URI);
27
+ if (!attr)
28
+ return {};
29
+ if (attr.value === "true")
30
+ return { value: true, raw: attr.value };
31
+ if (attr.value === "false")
32
+ return { value: false, raw: attr.value };
33
+ warnings.push(`${context}: android:${localName} has an unresolved or malformed value "${attr.value}" (line ${attr.location.line}) — not normalized to true or false.`);
34
+ return { raw: attr.value };
35
+ }
36
+ function parseIntAttr(element, localName, warnings, context) {
37
+ const attr = getAttribute(element, localName, ANDROID_NAMESPACE_URI);
38
+ if (!attr)
39
+ return undefined;
40
+ const parsed = Number.parseInt(attr.value, 10);
41
+ if (Number.isNaN(parsed)) {
42
+ warnings.push(`${context}: android:${localName} is not a resolvable integer ("${attr.value}", line ${attr.location.line}).`);
43
+ return undefined;
44
+ }
45
+ return parsed;
46
+ }
47
+ const LAUNCHER_ACTION = "android.intent.action.MAIN";
48
+ const LAUNCHER_CATEGORY = "android.intent.category.LAUNCHER";
49
+ const VIEW_ACTION = "android.intent.action.VIEW";
50
+ const BROWSABLE_CATEGORY = "android.intent.category.BROWSABLE";
51
+ function parseIntentFilter(element, warnings) {
52
+ const actions = findChildren(element, "action")
53
+ .map((a) => androidAttr(a, "name"))
54
+ .filter((v) => Boolean(v));
55
+ const categories = findChildren(element, "category")
56
+ .map((c) => androidAttr(c, "name"))
57
+ .filter((v) => Boolean(v));
58
+ const autoVerifyResult = parseBooleanAttr(element, "autoVerify", warnings, "intent-filter");
59
+ const dataElements = findChildren(element, "data").map((d) => ({
60
+ dataScheme: androidAttr(d, "scheme"),
61
+ dataHost: androidAttr(d, "host"),
62
+ dataPath: androidAttr(d, "path"),
63
+ dataPathPrefix: androidAttr(d, "pathPrefix"),
64
+ dataPathPattern: androidAttr(d, "pathPattern"),
65
+ autoVerify: autoVerifyResult.value,
66
+ location: toLocation(d.location),
67
+ }));
68
+ const isDeepLinkCandidate = actions.includes(VIEW_ACTION) && (categories.includes(BROWSABLE_CATEGORY) || dataElements.length > 0);
69
+ for (const child of element.children) {
70
+ if (child.localName !== "action" && child.localName !== "category" && child.localName !== "data") {
71
+ warnings.push(`Unsupported intent-filter child element <${child.tagName}> at line ${child.location.line} (preserved as a warning, not parsed).`);
72
+ }
73
+ }
74
+ return {
75
+ filterData: dataElements,
76
+ actions,
77
+ categories,
78
+ dataElements,
79
+ autoVerify: autoVerifyResult.value,
80
+ isDeepLinkCandidate,
81
+ location: toLocation(element.location),
82
+ };
83
+ }
84
+ function parseComponent(kind, element, warnings) {
85
+ const context = `<${kind}> at line ${element.location.line}`;
86
+ const name = androidAttr(element, "name");
87
+ if (!name) {
88
+ warnings.push(`${context} is missing android:name.`);
89
+ }
90
+ const exportedResult = parseBooleanAttr(element, "exported", warnings, context);
91
+ const enabledResult = parseBooleanAttr(element, "enabled", warnings, context);
92
+ const permission = androidAttr(element, "permission");
93
+ const intentFilters = findChildren(element, "intent-filter").map((filterEl) => parseIntentFilter(filterEl, warnings));
94
+ const isLauncherActivity = (kind === "activity" || kind === "activity-alias") &&
95
+ intentFilters.some((filter) => filter.actions?.includes(LAUNCHER_ACTION) && filter.categories?.includes(LAUNCHER_CATEGORY));
96
+ const component = {
97
+ kind,
98
+ name: name ?? "",
99
+ exported: exportedResult.value,
100
+ exportedRaw: exportedResult.raw,
101
+ enabled: enabledResult.value,
102
+ permission,
103
+ intentFilters,
104
+ isLauncherActivity: isLauncherActivity || undefined,
105
+ location: toLocation(element.location),
106
+ };
107
+ if (kind === "provider") {
108
+ component.readPermission = androidAttr(element, "readPermission");
109
+ component.writePermission = androidAttr(element, "writePermission");
110
+ const authoritiesRaw = androidAttr(element, "authorities");
111
+ component.authorities = authoritiesRaw ? authoritiesRaw.split(";").map((s) => s.trim()).filter(Boolean) : undefined;
112
+ component.grantUriPermissions = parseBooleanAttr(element, "grantUriPermissions", warnings, context).value;
113
+ }
114
+ return component;
115
+ }
116
+ // Parses AndroidManifest.xml source text into the normalized contract. Pure
117
+ // (no filesystem access) so it can be unit tested directly against inline
118
+ // XML strings.
119
+ export function parseAndroidManifestSource(xmlText, manifestPath) {
120
+ const parseWarnings = [];
121
+ const unsupportedConstructs = [];
122
+ const parsed = parseXmlDocument(xmlText);
123
+ for (const warning of parsed.warnings) {
124
+ parseWarnings.push(`${warning.message} (line ${warning.location.line}, column ${warning.location.column})`);
125
+ }
126
+ if (!parsed.ok) {
127
+ parseWarnings.push(`Malformed XML: ${parsed.error.message} (line ${parsed.error.location.line}, column ${parsed.error.location.column})`);
128
+ return {
129
+ manifestPath,
130
+ application: {},
131
+ permissions: [],
132
+ usesFeatures: [],
133
+ activities: [],
134
+ activityAliases: [],
135
+ services: [],
136
+ receivers: [],
137
+ providers: [],
138
+ deepLinks: [],
139
+ parseWarnings,
140
+ unsupportedConstructs,
141
+ };
142
+ }
143
+ const root = parsed.root;
144
+ if (root.localName !== "manifest") {
145
+ parseWarnings.push(`Root element is <${root.tagName}>, not <manifest> — manifest could not be interpreted.`);
146
+ return {
147
+ manifestPath,
148
+ application: {},
149
+ permissions: [],
150
+ usesFeatures: [],
151
+ activities: [],
152
+ activityAliases: [],
153
+ services: [],
154
+ receivers: [],
155
+ providers: [],
156
+ deepLinks: [],
157
+ parseWarnings,
158
+ unsupportedConstructs,
159
+ };
160
+ }
161
+ const packageName = getUnprefixedAttribute(root, "package");
162
+ const permissionElementNames = [
163
+ ["uses-permission", "uses-permission"],
164
+ ["uses-permission-sdk-23", "uses-permission-sdk-23"],
165
+ ["uses-permission-sdk-m", "uses-permission-sdk-m"],
166
+ ];
167
+ const permissions = [];
168
+ for (const [elementName, sourceElement] of permissionElementNames) {
169
+ for (const el of findChildren(root, elementName)) {
170
+ const name = androidAttr(el, "name");
171
+ if (!name) {
172
+ parseWarnings.push(`<${elementName}> at line ${el.location.line} is missing android:name.`);
173
+ continue;
174
+ }
175
+ permissions.push({
176
+ name,
177
+ maxSdkVersion: parseIntAttr(el, "maxSdkVersion", parseWarnings, `<${elementName}> "${name}"`),
178
+ sourceElement,
179
+ location: toLocation(el.location),
180
+ });
181
+ }
182
+ }
183
+ const usesFeatures = findChildren(root, "uses-feature")
184
+ .map((el) => {
185
+ const name = androidAttr(el, "name");
186
+ if (!name) {
187
+ parseWarnings.push(`<uses-feature> at line ${el.location.line} is missing android:name.`);
188
+ return undefined;
189
+ }
190
+ return {
191
+ name,
192
+ required: parseBooleanAttr(el, "required", parseWarnings, `<uses-feature> "${name}"`).value,
193
+ location: toLocation(el.location),
194
+ };
195
+ })
196
+ .filter((v) => v !== undefined);
197
+ const applicationElements = findChildren(root, "application");
198
+ const applicationEl = applicationElements[0];
199
+ if (!applicationEl) {
200
+ unsupportedConstructs.push("Manifest has no <application> element.");
201
+ }
202
+ if (applicationElements.length > 1) {
203
+ parseWarnings.push(`Manifest declares ${applicationElements.length} <application> elements; only the first was used.`);
204
+ }
205
+ const application = applicationEl
206
+ ? {
207
+ name: androidAttr(applicationEl, "name"),
208
+ label: androidAttr(applicationEl, "label"),
209
+ iconRef: androidAttr(applicationEl, "icon"),
210
+ enabled: parseBooleanAttr(applicationEl, "enabled", parseWarnings, "<application>").value,
211
+ permission: androidAttr(applicationEl, "permission"),
212
+ allowBackup: parseBooleanAttr(applicationEl, "allowBackup", parseWarnings, "<application>").value,
213
+ debuggable: parseBooleanAttr(applicationEl, "debuggable", parseWarnings, "<application>").value,
214
+ usesCleartextTraffic: parseBooleanAttr(applicationEl, "usesCleartextTraffic", parseWarnings, "<application>").value,
215
+ networkSecurityConfigRef: androidAttr(applicationEl, "networkSecurityConfig"),
216
+ }
217
+ : {};
218
+ const activities = applicationEl ? findChildren(applicationEl, "activity").map((el) => parseComponent("activity", el, parseWarnings)) : [];
219
+ const activityAliases = applicationEl
220
+ ? findChildren(applicationEl, "activity-alias").map((el) => parseComponent("activity-alias", el, parseWarnings))
221
+ : [];
222
+ const services = applicationEl ? findChildren(applicationEl, "service").map((el) => parseComponent("service", el, parseWarnings)) : [];
223
+ const receivers = applicationEl ? findChildren(applicationEl, "receiver").map((el) => parseComponent("receiver", el, parseWarnings)) : [];
224
+ const providers = applicationEl ? findChildren(applicationEl, "provider").map((el) => parseComponent("provider", el, parseWarnings)) : [];
225
+ if (applicationEl) {
226
+ const knownChildNames = new Set(["activity", "activity-alias", "service", "receiver", "provider", "meta-data"]);
227
+ for (const child of applicationEl.children) {
228
+ if (!knownChildNames.has(child.localName)) {
229
+ unsupportedConstructs.push(`Unsupported <application> child element <${child.tagName}> at line ${child.location.line}.`);
230
+ }
231
+ }
232
+ }
233
+ const deepLinks = [];
234
+ let launcherActivityName;
235
+ for (const component of [...activities, ...activityAliases]) {
236
+ if (component.isLauncherActivity && !launcherActivityName) {
237
+ launcherActivityName = component.name;
238
+ }
239
+ for (const filter of component.intentFilters) {
240
+ if (filter.isDeepLinkCandidate) {
241
+ deepLinks.push(...(filter.dataElements ?? filter.filterData));
242
+ }
243
+ }
244
+ }
245
+ return {
246
+ manifestPath,
247
+ packageName,
248
+ application,
249
+ permissions,
250
+ usesFeatures,
251
+ activities,
252
+ activityAliases,
253
+ services,
254
+ receivers,
255
+ providers,
256
+ deepLinks,
257
+ launcherActivityName,
258
+ parseWarnings,
259
+ unsupportedConstructs,
260
+ };
261
+ }
262
+ // Reads and parses a manifest file. `manifestRelativePath` must already be
263
+ // target-relative (as returned by AndroidDetectionResult.manifestPaths); this
264
+ // re-validates containment before reading so a caller-supplied path can never
265
+ // escape targetRoot.
266
+ export function parseAndroidManifestFile(targetRoot, manifestRelativePath) {
267
+ const resolvedRoot = path.resolve(targetRoot);
268
+ let absolutePath;
269
+ try {
270
+ absolutePath = resolveWithinRoot(resolvedRoot, manifestRelativePath);
271
+ }
272
+ catch (error) {
273
+ return {
274
+ manifestPath: manifestRelativePath,
275
+ application: {},
276
+ permissions: [],
277
+ usesFeatures: [],
278
+ activities: [],
279
+ activityAliases: [],
280
+ services: [],
281
+ receivers: [],
282
+ providers: [],
283
+ deepLinks: [],
284
+ parseWarnings: [`Manifest path rejected: ${error instanceof Error ? error.message : "path escapes target root"}`],
285
+ unsupportedConstructs: [],
286
+ };
287
+ }
288
+ const normalizedPath = relativeWithinRoot(resolvedRoot, absolutePath);
289
+ let xmlText;
290
+ try {
291
+ xmlText = readFileSync(absolutePath, "utf8");
292
+ }
293
+ catch (error) {
294
+ return {
295
+ manifestPath: normalizedPath,
296
+ application: {},
297
+ permissions: [],
298
+ usesFeatures: [],
299
+ activities: [],
300
+ activityAliases: [],
301
+ services: [],
302
+ receivers: [],
303
+ providers: [],
304
+ deepLinks: [],
305
+ parseWarnings: [`Could not read manifest file: ${error instanceof Error ? error.message : "unknown error"}`],
306
+ unsupportedConstructs: [],
307
+ };
308
+ }
309
+ return parseAndroidManifestSource(xmlText, normalizedPath);
310
+ }
311
+ const KNOWN_SOURCE_SET_KINDS = ["main", "debug", "release", "androidTest", "test"];
312
+ function inferSourceSetKind(manifestPath) {
313
+ const segments = manifestPath.split("/");
314
+ const srcIndex = segments.indexOf("src");
315
+ const sourceSetSegment = srcIndex !== -1 ? segments[srcIndex + 1] : undefined;
316
+ return KNOWN_SOURCE_SET_KINDS.includes(sourceSetSegment ?? "")
317
+ ? sourceSetSegment
318
+ : "other";
319
+ }
320
+ function inferModulePath(manifestPath) {
321
+ const srcIndex = manifestPath.indexOf("/src/");
322
+ return srcIndex === -1 ? undefined : manifestPath.slice(0, srcIndex);
323
+ }
324
+ // Parses every manifest path from a Batch 2 AndroidDetectionResult
325
+ // independently. One malformed manifest never prevents the others from being
326
+ // parsed. Does not merge manifests — each entry stands alone, tagged with its
327
+ // inferred module and source-set association for later batches to use.
328
+ export function parseAllAndroidManifests(targetRoot, detection) {
329
+ const moduleManifestMap = new Map();
330
+ for (const module of detection.modules) {
331
+ for (const manifestPath of module.manifestPaths) {
332
+ moduleManifestMap.set(manifestPath, module.path);
333
+ }
334
+ }
335
+ return [...detection.manifestPaths].sort((a, b) => a.localeCompare(b)).map((manifestPath) => ({
336
+ manifestPath,
337
+ modulePath: moduleManifestMap.get(manifestPath) ?? inferModulePath(manifestPath),
338
+ sourceSetKind: inferSourceSetKind(manifestPath),
339
+ manifest: parseAndroidManifestFile(targetRoot, manifestPath),
340
+ }));
341
+ }
@@ -0,0 +1,29 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 1 — AndroidManifest model foundation.
3
+ // v0.4.0 Batch 3 — narrow additive inherited-contract corrections needed by
4
+ // manifest parsing and the initial audits (see agents.txt Batch 3 section
5
+ // 6.7). Every change below only adds new optional fields/enum members or a
6
+ // new optional array — no existing required field, type, or enum value was
7
+ // removed or retyped, so all Batch 1/2 object literals remain valid:
8
+ // - AndroidManifestComponentKind gained "activity-alias" (section 7.6).
9
+ // - AndroidManifestComponent gained optional `exportedRaw` so a malformed
10
+ // android:exported value can be distinguished from "unspecified"
11
+ // (`exported` stays undefined in both cases; `exportedRaw` preserves the
12
+ // literal attribute text when present) — section 7.7's invariant that
13
+ // malformed values must never be normalized to false requires this.
14
+ // - AndroidManifestModel gained optional `activityAliases` (section 7.6).
15
+ // - AndroidIntentFilter gained optional `actions`/`categories`/
16
+ // `dataElements` so multiple sibling <action>/<category>/<data> children
17
+ // can be preserved independently (section 7.8) without discarding the
18
+ // original `filterData` field, which Batch 1 tests and any external
19
+ // consumer may already rely on; the parser keeps populating both.
20
+ // - AndroidIntentFilterData gained optional `location` (section 7.11).
21
+ // - AndroidPermissionDeclaration gained optional `sourceElement` so
22
+ // uses-permission / uses-permission-sdk-23 / uses-permission-sdk-m are
23
+ // not conflated (section 7.4, ANDROID-B3-03).
24
+ //
25
+ // Normalized data contracts consumed by the manifest parser and the
26
+ // permission/component/deep-link auditors added in later batches. This file
27
+ // defines shape only — no XML parsing and no finding generation happen here.
28
+ // ---------------------------------------------------------------------------
29
+ export {};
@@ -0,0 +1,313 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 3 — bounded, deterministic, dependency-free XML parser for
3
+ // AndroidManifest.xml.
4
+ //
5
+ // No XML parsing dependency exists in this repository (see package.json —
6
+ // zero runtime dependencies), so this hand-written recursive-descent scanner
7
+ // avoids adding one, per agents.txt Batch 3 section 7.2 ("prefer a small
8
+ // bounded parser implementation ... Do not use brittle global regular
9
+ // expressions as the primary XML parser"). This is a manual character
10
+ // scanner (no regex-based tokenization), tracks line/column for every
11
+ // element and attribute, and resolves XML namespace URIs from `xmlns`/
12
+ // `xmlns:*` declarations rather than assuming any particular prefix — so an
13
+ // `android:` attribute is recognized even if a manifest (unusually)
14
+ // re-prefixes the Android namespace.
15
+ //
16
+ // Deliberately NOT implemented (out of scope for a manifest-only parser):
17
+ // DTD/entity resolution beyond skipping DOCTYPE, full XML 1.1 conformance,
18
+ // processing-instruction data, and CDATA content extraction (CDATA is
19
+ // skipped as opaque text — AndroidManifest.xml has no meaningful use of it).
20
+ // ---------------------------------------------------------------------------
21
+ export const ANDROID_NAMESPACE_URI = "http://schemas.android.com/apk/res/android";
22
+ class XmlSyntaxError extends Error {
23
+ location;
24
+ constructor(message, location) {
25
+ super(message);
26
+ this.location = location;
27
+ }
28
+ }
29
+ const NAME_START_PATTERN = /[A-Za-z_:]/;
30
+ const NAME_CHAR_PATTERN = /[A-Za-z0-9_.:-]/;
31
+ function isNameStartChar(ch) {
32
+ return ch !== undefined && NAME_START_PATTERN.test(ch);
33
+ }
34
+ function isNameChar(ch) {
35
+ return ch !== undefined && NAME_CHAR_PATTERN.test(ch);
36
+ }
37
+ function isWhitespace(ch) {
38
+ return ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
39
+ }
40
+ class Scanner {
41
+ text;
42
+ pos = 0;
43
+ line = 1;
44
+ column = 1;
45
+ constructor(text) {
46
+ // Strip a UTF-8 BOM if present so it doesn't count as document content.
47
+ this.text = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
48
+ }
49
+ loc() {
50
+ return { line: this.line, column: this.column };
51
+ }
52
+ eof() {
53
+ return this.pos >= this.text.length;
54
+ }
55
+ peek(offset = 0) {
56
+ return this.text[this.pos + offset];
57
+ }
58
+ startsWith(literal) {
59
+ return this.text.startsWith(literal, this.pos);
60
+ }
61
+ advance() {
62
+ const ch = this.text[this.pos];
63
+ if (ch === undefined) {
64
+ throw new XmlSyntaxError("Unexpected end of document", this.loc());
65
+ }
66
+ this.pos += 1;
67
+ if (ch === "\n") {
68
+ this.line += 1;
69
+ this.column = 1;
70
+ }
71
+ else {
72
+ this.column += 1;
73
+ }
74
+ return ch;
75
+ }
76
+ skipWhitespace() {
77
+ while (!this.eof() && isWhitespace(this.peek()))
78
+ this.advance();
79
+ }
80
+ expect(literal) {
81
+ if (!this.startsWith(literal)) {
82
+ throw new XmlSyntaxError(`Expected "${literal}"`, this.loc());
83
+ }
84
+ for (let i = 0; i < literal.length; i++)
85
+ this.advance();
86
+ }
87
+ skipUntil(literal) {
88
+ while (!this.eof() && !this.startsWith(literal))
89
+ this.advance();
90
+ if (this.eof()) {
91
+ throw new XmlSyntaxError(`Unterminated construct, expected "${literal}"`, this.loc());
92
+ }
93
+ this.expect(literal);
94
+ }
95
+ }
96
+ function resolveNamespace(prefix, scope) {
97
+ return scope.get(prefix ?? "");
98
+ }
99
+ function splitQualifiedName(qualifiedName) {
100
+ const colonIndex = qualifiedName.indexOf(":");
101
+ if (colonIndex === -1)
102
+ return { localName: qualifiedName };
103
+ return { prefix: qualifiedName.slice(0, colonIndex), localName: qualifiedName.slice(colonIndex + 1) };
104
+ }
105
+ function readName(scanner) {
106
+ const start = scanner.loc();
107
+ if (!isNameStartChar(scanner.peek())) {
108
+ throw new XmlSyntaxError("Expected a name", start);
109
+ }
110
+ let name = "";
111
+ while (isNameChar(scanner.peek()))
112
+ name += scanner.advance();
113
+ return name;
114
+ }
115
+ function readQuotedValue(scanner) {
116
+ const quote = scanner.peek();
117
+ if (quote !== `"` && quote !== `'`) {
118
+ throw new XmlSyntaxError("Expected a quoted attribute value", scanner.loc());
119
+ }
120
+ scanner.advance();
121
+ let value = "";
122
+ while (!scanner.eof() && scanner.peek() !== quote) {
123
+ value += scanner.advance();
124
+ }
125
+ if (scanner.eof()) {
126
+ throw new XmlSyntaxError("Unterminated attribute value", scanner.loc());
127
+ }
128
+ scanner.advance();
129
+ return decodeXmlEntities(value);
130
+ }
131
+ function decodeXmlEntities(raw) {
132
+ return raw
133
+ .replace(/&lt;/g, "<")
134
+ .replace(/&gt;/g, ">")
135
+ .replace(/&apos;/g, "'")
136
+ .replace(/&quot;/g, '"')
137
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))
138
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
139
+ .replace(/&amp;/g, "&");
140
+ }
141
+ function skipMisc(scanner, warnings) {
142
+ // Skips whitespace, comments, the XML declaration/processing instructions,
143
+ // and DOCTYPE declarations that may appear between/around elements.
144
+ for (;;) {
145
+ scanner.skipWhitespace();
146
+ if (scanner.startsWith("<!--")) {
147
+ scanner.skipUntil("-->");
148
+ continue;
149
+ }
150
+ if (scanner.startsWith("<?")) {
151
+ scanner.skipUntil("?>");
152
+ continue;
153
+ }
154
+ if (scanner.startsWith("<!DOCTYPE") || scanner.startsWith("<!doctype")) {
155
+ warnings.push({ message: "DOCTYPE declaration encountered and skipped (not evaluated)", location: scanner.loc() });
156
+ scanner.skipUntil(">");
157
+ continue;
158
+ }
159
+ if (scanner.startsWith("<![CDATA[")) {
160
+ scanner.skipUntil("]]>");
161
+ continue;
162
+ }
163
+ break;
164
+ }
165
+ }
166
+ function parseAttributes(scanner) {
167
+ const rawAttributes = [];
168
+ for (;;) {
169
+ scanner.skipWhitespace();
170
+ const next = scanner.peek();
171
+ if (next === undefined || next === "/" || next === ">")
172
+ break;
173
+ const location = scanner.loc();
174
+ const rawName = readName(scanner);
175
+ scanner.skipWhitespace();
176
+ scanner.expect("=");
177
+ scanner.skipWhitespace();
178
+ const value = readQuotedValue(scanner);
179
+ rawAttributes.push({ rawName, value, location });
180
+ }
181
+ // Build the namespace scope contributed by this element's own xmlns/xmlns:*
182
+ // attributes so attribute namespace resolution can use it immediately
183
+ // (XML allows an element to both declare and use a namespace prefix).
184
+ const localScope = new Map();
185
+ for (const attr of rawAttributes) {
186
+ if (attr.rawName === "xmlns") {
187
+ localScope.set("", attr.value);
188
+ }
189
+ else if (attr.rawName.startsWith("xmlns:")) {
190
+ localScope.set(attr.rawName.slice("xmlns:".length), attr.value);
191
+ }
192
+ }
193
+ return { rawAttributes, localScope };
194
+ }
195
+ // Resolves each raw attribute's namespace URI against the combined
196
+ // (ancestor + this element's own) namespace scope. `xmlns`/`xmlns:*`
197
+ // attributes themselves are preserved as regular attributes (some manifest
198
+ // tooling inspects them) but are not namespace-resolved.
199
+ function resolveAttributeNamespaces(rawAttributes, scope, warnings) {
200
+ return rawAttributes.map(({ rawName, value, location }) => {
201
+ const { prefix, localName } = splitQualifiedName(rawName);
202
+ if (rawName === "xmlns" || rawName.startsWith("xmlns:")) {
203
+ return { rawName, localName, value, location };
204
+ }
205
+ if (prefix === undefined) {
206
+ return { rawName, localName, value, location };
207
+ }
208
+ const namespaceUri = resolveNamespace(prefix, scope);
209
+ if (namespaceUri === undefined) {
210
+ warnings.push({ message: `Unresolved namespace prefix "${prefix}" on attribute "${rawName}"`, location });
211
+ }
212
+ return { rawName, prefix, localName, namespaceUri, value, location };
213
+ });
214
+ }
215
+ function parseElement(scanner, parentScope, warnings) {
216
+ const location = scanner.loc();
217
+ scanner.expect("<");
218
+ const tagName = readName(scanner);
219
+ const { rawAttributes, localScope } = parseAttributes(scanner);
220
+ const scope = localScope.size > 0 ? new Map([...parentScope, ...localScope]) : parentScope;
221
+ const attributes = resolveAttributeNamespaces(rawAttributes, scope, warnings);
222
+ const { prefix, localName } = splitQualifiedName(tagName);
223
+ const namespaceUri = prefix !== undefined ? resolveNamespace(prefix, scope) : resolveNamespace(undefined, scope);
224
+ scanner.skipWhitespace();
225
+ if (scanner.startsWith("/>")) {
226
+ scanner.expect("/>");
227
+ return { tagName, prefix, localName, namespaceUri, attributes, children: [], location };
228
+ }
229
+ scanner.expect(">");
230
+ const children = [];
231
+ for (;;) {
232
+ skipTextAndMisc(scanner, warnings);
233
+ if (scanner.startsWith("</")) {
234
+ scanner.expect("</");
235
+ const closingName = readName(scanner);
236
+ scanner.skipWhitespace();
237
+ scanner.expect(">");
238
+ if (closingName !== tagName) {
239
+ throw new XmlSyntaxError(`Mismatched closing tag: expected "</${tagName}>" but found "</${closingName}>"`, location);
240
+ }
241
+ break;
242
+ }
243
+ if (scanner.eof()) {
244
+ throw new XmlSyntaxError(`Unterminated element "<${tagName}>" (missing closing tag)`, location);
245
+ }
246
+ if (scanner.peek() === "<" && isNameStartChar(scanner.peek(1))) {
247
+ children.push(parseElement(scanner, scope, warnings));
248
+ continue;
249
+ }
250
+ // Any other content (unexpected `<` not matching a comment/PI/CDATA/
251
+ // element/closing-tag pattern) is treated as malformed input.
252
+ throw new XmlSyntaxError("Unexpected content while looking for a child element or closing tag", scanner.loc());
253
+ }
254
+ return { tagName, prefix, localName, namespaceUri, attributes, children, location };
255
+ }
256
+ function skipTextAndMisc(scanner, warnings) {
257
+ for (;;) {
258
+ if (scanner.startsWith("<!--")) {
259
+ scanner.skipUntil("-->");
260
+ continue;
261
+ }
262
+ if (scanner.startsWith("<![CDATA[")) {
263
+ scanner.skipUntil("]]>");
264
+ continue;
265
+ }
266
+ if (scanner.startsWith("<?")) {
267
+ scanner.skipUntil("?>");
268
+ continue;
269
+ }
270
+ if (scanner.peek() === "<")
271
+ break;
272
+ if (scanner.eof())
273
+ break;
274
+ // Plain text content between elements — not meaningful for manifest
275
+ // structure, so it is discarded rather than stored.
276
+ scanner.advance();
277
+ continue;
278
+ }
279
+ }
280
+ // Parses `xmlText` into a small element tree. Never throws: all failures are
281
+ // reported via the `ok: false` branch with a bounded message and location.
282
+ export function parseXmlDocument(xmlText) {
283
+ const warnings = [];
284
+ try {
285
+ const scanner = new Scanner(xmlText);
286
+ skipMisc(scanner, warnings);
287
+ if (scanner.eof() || scanner.peek() !== "<") {
288
+ return { ok: false, error: { message: "No root element found", location: scanner.loc() }, warnings };
289
+ }
290
+ const root = parseElement(scanner, new Map(), warnings);
291
+ skipMisc(scanner, warnings);
292
+ if (!scanner.eof()) {
293
+ warnings.push({ message: "Unexpected trailing content after the root element was ignored", location: scanner.loc() });
294
+ }
295
+ return { ok: true, root, warnings };
296
+ }
297
+ catch (error) {
298
+ if (error instanceof XmlSyntaxError) {
299
+ return { ok: false, error: { message: error.message, location: error.location }, warnings };
300
+ }
301
+ return {
302
+ ok: false,
303
+ error: { message: error instanceof Error ? error.message : "Unknown XML parse error", location: { line: 1, column: 1 } },
304
+ warnings,
305
+ };
306
+ }
307
+ }
308
+ export function findChildren(element, localName, namespaceUri) {
309
+ return element.children.filter((child) => child.localName === localName && (namespaceUri === undefined || child.namespaceUri === namespaceUri));
310
+ }
311
+ export function getAttribute(element, localName, namespaceUri = ANDROID_NAMESPACE_URI) {
312
+ return element.attributes.find((attr) => attr.localName === localName && attr.namespaceUri === namespaceUri);
313
+ }