@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
@@ -42,6 +42,7 @@ export const ARCHITECTURE_DRIFT_DETECTOR = {
42
42
  },
43
43
  run: (ctx) => {
44
44
  const issues = [];
45
+ const hasAndroidValidationSource = fs.existsSync(path.join(ctx.target.rootPath, "src", "mobile", "android", "validate", "validateAndroidTarget.ts"));
45
46
  const currentStateDocs = ctx.inventory.docsFiles.filter((f) => CURRENT_STATE_DOC_BASENAMES.has(path.basename(f.relativePath).toLowerCase()));
46
47
  for (const docFile of currentStateDocs) {
47
48
  const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
@@ -61,7 +62,7 @@ export const ARCHITECTURE_DRIFT_DETECTOR = {
61
62
  const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
62
63
  if (content === null)
63
64
  continue;
64
- issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content));
65
+ issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content, hasAndroidValidationSource));
65
66
  }
66
67
  return deduplicateIssuesById(issues);
67
68
  },
@@ -227,13 +228,15 @@ function findMajorDirsAbsentFromArchitectureDoc(ctx, docRelativePath, content) {
227
228
  }
228
229
  return issues;
229
230
  }
230
- function findUnsupportedCurrentClaims(docRelativePath, content) {
231
+ function findUnsupportedCurrentClaims(docRelativePath, content, hasAndroidValidationSource) {
231
232
  const issues = [];
232
233
  const subjects = [
233
234
  { id: "manual-pentest", label: "manual pentest", pattern: "manual pentest" },
234
235
  { id: "android-validation", label: "Android/mobile validation", pattern: "(?:android (?:validation|support))" },
235
236
  ];
236
237
  for (const subject of subjects) {
238
+ if (subject.id === "android-validation" && hasAndroidValidationSource)
239
+ continue;
237
240
  const pattern = currentClaimPatternFor(subject.pattern);
238
241
  const match = pattern.exec(content);
239
242
  if (!match)
@@ -67,6 +67,7 @@ export const SECURITY_VALIDATION_ASSUMPTION_ROT_DETECTOR = {
67
67
  const pkgVersion = ctx.sourceOfTruth.package?.version ?? null;
68
68
  const hasSecurityValidationSourceDir = ctx.sourceOfTruth.security.hasSecurityValidationSourceDir;
69
69
  const hasAttackScenariosDir = fs.existsSync(path.join(ctx.target.rootPath, "src", "securityValidation", "attackScenarios"));
70
+ const hasAndroidValidationSource = fs.existsSync(path.join(ctx.target.rootPath, "src", "mobile", "android", "validate", "validateAndroidTarget.ts"));
70
71
  for (const docFile of ctx.inventory.docsFiles) {
71
72
  const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
72
73
  if (content === null)
@@ -75,7 +76,7 @@ export const SECURITY_VALIDATION_ASSUMPTION_ROT_DETECTOR = {
75
76
  issues.push(...findSkippedEqualsPassed(docFile.relativePath, content));
76
77
  issues.push(...findExhaustiveSecretClaim(docFile.relativePath, content));
77
78
  issues.push(...findCompleteNetworkIsolationClaim(docFile.relativePath, content));
78
- issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content));
79
+ issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content, hasAndroidValidationSource));
79
80
  if (pkgVersion)
80
81
  issues.push(...findStaleV021SecurityMention(docFile.relativePath, content, pkgVersion));
81
82
  if (hasSecurityValidationSourceDir && hasAttackScenariosDir) {
@@ -237,13 +238,15 @@ function findCompleteNetworkIsolationClaim(relativePath, content) {
237
238
  }),
238
239
  ];
239
240
  }
240
- function findUnsupportedCurrentClaims(relativePath, content) {
241
+ function findUnsupportedCurrentClaims(relativePath, content, hasAndroidValidationSource) {
241
242
  const issues = [];
242
243
  const subjects = [
243
244
  { id: "manual-pentest", label: "manual pentest", pattern: "manual pentest" },
244
245
  { id: "android-validation", label: "Android/mobile validation", pattern: "(?:android (?:validation|support)|mobile validation)" },
245
246
  ];
246
247
  for (const subject of subjects) {
248
+ if (subject.id === "android-validation" && hasAndroidValidationSource)
249
+ continue;
247
250
  const match = currentClaimPatternFor(subject.pattern).exec(content);
248
251
  if (!match)
249
252
  continue;
@@ -41,17 +41,18 @@ export async function collectSourceFacts(targetRoot, inventory, registry = DEFAU
41
41
  // filtering preserves that order, so no re-sort is needed here.
42
42
  const eligible = inventory.files.filter((entry) => ANALYZABLE_ROLES.has(entry.role));
43
43
  for (const entry of eligible) {
44
+ const relativePath = normalizeRelativePath(entry.relativePath);
44
45
  const analyzer = selectLanguageAnalyzer(registry, entry.language, entry.extension);
45
46
  if (!analyzer) {
46
- files.push(buildFallbackFacts(entry));
47
+ files.push(buildFallbackFacts(entry, relativePath));
47
48
  continue;
48
49
  }
49
- const absolutePath = path.join(resolvedRoot, entry.relativePath);
50
+ const absolutePath = resolveInventoryFilePath(resolvedRoot, relativePath);
50
51
  try {
51
52
  const content = fs.readFileSync(absolutePath, "utf8");
52
53
  const facts = await analyzer.analyzeFile({
53
54
  targetRoot: resolvedRoot,
54
- relativePath: entry.relativePath,
55
+ relativePath,
55
56
  absolutePath,
56
57
  content,
57
58
  inventoryEntry: entry,
@@ -65,13 +66,13 @@ export async function collectSourceFacts(targetRoot, inventory, registry = DEFAU
65
66
  const diagnostic = {
66
67
  severity: "error",
67
68
  code: "analyzer-error",
68
- message: `Analyzer "${analyzer.id}" failed on "${entry.relativePath}": ${message}`,
69
- path: entry.relativePath,
69
+ message: `Analyzer "${analyzer.id}" failed on "${relativePath}": ${message}`,
70
+ path: relativePath,
70
71
  analyzerId: analyzer.id,
71
72
  };
72
73
  analyzerDiagnostics.push(diagnostic);
73
74
  files.push({
74
- relativePath: entry.relativePath,
75
+ relativePath,
75
76
  language: entry.language,
76
77
  role: entry.role,
77
78
  parseStatus: "parse-error",
@@ -94,7 +95,7 @@ export async function collectSourceFacts(targetRoot, inventory, registry = DEFAU
94
95
  warnings,
95
96
  };
96
97
  }
97
- function buildFallbackFacts(entry) {
98
+ function buildFallbackFacts(entry, relativePath) {
98
99
  const known = KNOWN_LANGUAGES.has(entry.language);
99
100
  const parseStatus = known ? "file-level-only" : "unsupported";
100
101
  const diagnostic = known
@@ -102,18 +103,18 @@ function buildFallbackFacts(entry) {
102
103
  severity: "info",
103
104
  code: "no-analyzer-registered",
104
105
  message: `No language analyzer is registered for "${entry.language}" yet; only inventory-level facts are available.`,
105
- path: entry.relativePath,
106
+ path: relativePath,
106
107
  analyzerId: null,
107
108
  }
108
109
  : {
109
110
  severity: "info",
110
111
  code: "unsupported-language",
111
112
  message: `Language "${entry.language}" is not supported for source-fact extraction.`,
112
- path: entry.relativePath,
113
+ path: relativePath,
113
114
  analyzerId: null,
114
115
  };
115
116
  return {
116
- relativePath: entry.relativePath,
117
+ relativePath,
117
118
  language: entry.language,
118
119
  role: entry.role,
119
120
  parseStatus,
@@ -126,6 +127,16 @@ function buildFallbackFacts(entry) {
126
127
  diagnostics: [diagnostic],
127
128
  };
128
129
  }
130
+ function normalizeRelativePath(relativePath) {
131
+ const normalized = path.posix.normalize(relativePath.replace(/\\/g, "/"));
132
+ if (normalized === ".")
133
+ return "";
134
+ return normalized.replace(/^\/+/, "").replace(/^\.\//, "");
135
+ }
136
+ function resolveInventoryFilePath(root, relativePath) {
137
+ const parts = relativePath.split("/").filter((part) => part.length > 0);
138
+ return parts.length === 0 ? root : path.resolve(root, ...parts);
139
+ }
129
140
  function countByLanguage(files) {
130
141
  const counts = Object.fromEntries(NORMALIZED_LANGUAGES.map((l) => [l, 0]));
131
142
  for (const file of files) {
@@ -0,0 +1,68 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 3 — Android SecurityFinding factory.
3
+ //
4
+ // Reuses SecurityFinding as-is (no parallel Android finding schema). Android
5
+ // findings use category "static-scan" — an existing SecurityCheckCategory
6
+ // value that already covers static-analysis-derived findings (CodeQL,
7
+ // Semgrep) and accurately describes manifest analysis, so no widening of the
8
+ // shared, heavily-tested SecurityCheckCategory union was needed. Per-finding
9
+ // audit family is instead carried by the owning AndroidCheckResult.category
10
+ // (android-permissions/android-components/android-intent-filters/
11
+ // android-deep-links) and by the finding id/title.
12
+ //
13
+ // SecurityFinding has no `confidence` or structured `location` field (the
14
+ // existing codebase convention, e.g. AttackResult, keeps confidence on the
15
+ // *check result*, not the individual finding — see
16
+ // src/securityValidation/attackScenarios/attackResult.ts). Android check
17
+ // results already carry `confidence: MobileConfidence` (Batch 1). Per-finding
18
+ // confidence and location are therefore folded into the free-text `evidence`
19
+ // field here, consistent with how existing scenario code joins evidence
20
+ // previews into that same field.
21
+ // ---------------------------------------------------------------------------
22
+ function slug(value) {
23
+ return value
24
+ .replace(/[^a-zA-Z0-9]+/g, "-")
25
+ .replace(/^-+|-+$/g, "")
26
+ .toLowerCase();
27
+ }
28
+ // Deterministic, content-derived id — never depends on a counter, timestamp,
29
+ // or enumeration order, so equivalent evidence always yields the same id.
30
+ function buildFindingId(ruleId, manifestPath, identity, location) {
31
+ const parts = [ruleId, slug(manifestPath), identity ? slug(identity) : undefined, location?.line !== undefined ? `l${location.line}` : undefined].filter((part) => Boolean(part));
32
+ return parts.join("--");
33
+ }
34
+ function defaultReleaseImpact(severity) {
35
+ switch (severity) {
36
+ case "blocker":
37
+ return "Must fix before release";
38
+ case "major":
39
+ return "Should fix before release";
40
+ case "minor":
41
+ return "Review recommended before release";
42
+ case "informational":
43
+ return "No release impact; informational only";
44
+ case "skipped":
45
+ return "Not evaluated";
46
+ }
47
+ }
48
+ export function makeAndroidFinding(input) {
49
+ const id = buildFindingId(input.ruleId, input.manifestPath, input.identity, input.location);
50
+ const locationSuffix = input.location?.line !== undefined ? `:${input.location.line}` : "";
51
+ const evidenceParts = [
52
+ `confidence=${input.confidence}`,
53
+ input.identity ? `identity=${input.identity}` : undefined,
54
+ `location=${input.manifestPath}${locationSuffix}`,
55
+ ...(input.evidenceDetails ?? []),
56
+ ].filter((part) => Boolean(part));
57
+ return {
58
+ id,
59
+ title: input.title,
60
+ severity: input.severity,
61
+ category: "static-scan",
62
+ description: input.description,
63
+ evidence: evidenceParts.join(" | "),
64
+ affectedFiles: [input.manifestPath],
65
+ recommendation: input.recommendation,
66
+ releaseImpact: input.releaseImpact ?? defaultReleaseImpact(input.severity),
67
+ };
68
+ }
@@ -0,0 +1,97 @@
1
+ function isMalformed(entry) {
2
+ return entry.manifest.parseWarnings.some((w) => w.startsWith("Malformed XML:"));
3
+ }
4
+ export function buildAndroidManifestCheckResult(input) {
5
+ const { id, category, title, detection, manifests, findings, evidence, warnings } = input;
6
+ const sourcePaths = manifests.map((m) => m.manifestPath);
7
+ if (detection.projectKind === "non-android") {
8
+ return {
9
+ id,
10
+ category,
11
+ title,
12
+ status: "unsupported",
13
+ requirementLevel: "optional",
14
+ ran: false,
15
+ skipped: true,
16
+ skipInfo: {
17
+ checkId: id,
18
+ reason: "Target was not detected as an Android project.",
19
+ requirementLevel: "optional",
20
+ missingCapability: "android-detection",
21
+ verdictImpact: "does not apply to a non-Android target",
22
+ recommendedNextAction: "Re-run against an Android Gradle project, or verify project detection.",
23
+ },
24
+ evidence: [],
25
+ findings: [],
26
+ warnings: [],
27
+ errors: [],
28
+ sourcePaths: [],
29
+ confidence: "unknown",
30
+ environmentRequirements: [],
31
+ };
32
+ }
33
+ if (manifests.length === 0) {
34
+ return {
35
+ id,
36
+ category,
37
+ title,
38
+ status: "inconclusive",
39
+ requirementLevel: "required",
40
+ ran: true,
41
+ skipped: false,
42
+ evidence,
43
+ findings: [],
44
+ warnings: [...warnings, "No AndroidManifest.xml was found for this target; audit could not run."],
45
+ errors: [],
46
+ sourcePaths: [],
47
+ confidence: "unknown",
48
+ environmentRequirements: [],
49
+ };
50
+ }
51
+ const malformedEntries = manifests.filter(isMalformed);
52
+ const allMalformed = malformedEntries.length === manifests.length;
53
+ if (allMalformed) {
54
+ return {
55
+ id,
56
+ category,
57
+ title,
58
+ status: "error",
59
+ requirementLevel: "required",
60
+ ran: true,
61
+ skipped: false,
62
+ evidence,
63
+ findings: [],
64
+ warnings,
65
+ errors: malformedEntries.flatMap((entry) => entry.manifest.parseWarnings.filter((w) => w.startsWith("Malformed XML:"))),
66
+ sourcePaths,
67
+ confidence: "unknown",
68
+ environmentRequirements: [],
69
+ };
70
+ }
71
+ const confidence = malformedEntries.length > 0 ? "medium" : "high";
72
+ // Informational-only findings (e.g. "standard launcher activity is
73
+ // exported, as expected") are evidence/inventory, not a reportable issue —
74
+ // only minor/major/blocker findings cause a "failed" status, matching
75
+ // section 7.17's "completed and found no reportable issues may pass".
76
+ const hasReportableFinding = findings.some((finding) => finding.severity !== "informational");
77
+ const status = hasReportableFinding ? "failed" : "passed";
78
+ return {
79
+ id,
80
+ category,
81
+ title,
82
+ status,
83
+ requirementLevel: "required",
84
+ ran: true,
85
+ skipped: false,
86
+ evidence,
87
+ findings,
88
+ warnings: [
89
+ ...warnings,
90
+ ...malformedEntries.flatMap((entry) => entry.manifest.parseWarnings.filter((w) => w.startsWith("Malformed XML:"))),
91
+ ],
92
+ errors: [],
93
+ sourcePaths,
94
+ confidence,
95
+ environmentRequirements: [],
96
+ };
97
+ }
@@ -0,0 +1,183 @@
1
+ import { makeAndroidFinding } from "./androidFinding.js";
2
+ import { buildAndroidManifestCheckResult } from "./checkResultBuilder.js";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.4.0 Batch 3 — initial deep-link audit.
5
+ //
6
+ // Distinguishes web links (http/https) from custom-scheme deep links, flags
7
+ // missing host/scheme restrictions and broad path patterns, and treats
8
+ // BROWSABLE-without-VIEW and VIEW-without-BROWSABLE as separate "incomplete
9
+ // filter" observations (agents.txt Batch 3 section 7.16). Never claims
10
+ // Digital Asset Links verification, domain ownership, or exploitability —
11
+ // findings are framed as static exposure/configuration review, at most
12
+ // "major" for the narrow high-confidence case of an explicitly exported,
13
+ // unprotected, wildcard-path activity.
14
+ // ---------------------------------------------------------------------------
15
+ const VIEW_ACTION = "android.intent.action.VIEW";
16
+ const BROWSABLE_CATEGORY = "android.intent.category.BROWSABLE";
17
+ const WILDCARD_PATTERN = /[*.]{1,2}\*|\.\*/;
18
+ function isPlaceholder(value) {
19
+ return value !== undefined && (value.includes("${") || value.startsWith("@"));
20
+ }
21
+ function classifyScheme(dataElements) {
22
+ const schemes = dataElements.map((d) => d.dataScheme).filter((s) => Boolean(s));
23
+ if (schemes.length === 0)
24
+ return "none";
25
+ if (schemes.some((s) => s === "http" || s === "https"))
26
+ return "web";
27
+ return "custom";
28
+ }
29
+ function auditDeepLinkFilter(entry, kind, component, filter, findings) {
30
+ const identity = component.name || "(unnamed component)";
31
+ const actions = filter.actions ?? [];
32
+ const categories = filter.categories ?? [];
33
+ const dataElements = filter.dataElements ?? filter.filterData;
34
+ const hasView = actions.includes(VIEW_ACTION);
35
+ const hasBrowsable = categories.includes(BROWSABLE_CATEGORY);
36
+ if (!hasView && !hasBrowsable)
37
+ return;
38
+ if (hasBrowsable && !hasView) {
39
+ findings.push(makeAndroidFinding({
40
+ ruleId: "android-deep-link-browsable-without-view",
41
+ title: `BROWSABLE category without a VIEW action on ${kind} "${identity}"`,
42
+ severity: "informational",
43
+ confidence: "high",
44
+ description: "This <intent-filter> declares category BROWSABLE without action VIEW, so it will not actually be treated as a browsable web/deep link by the platform.",
45
+ manifestPath: entry.manifestPath,
46
+ identity,
47
+ location: filter.location,
48
+ }));
49
+ return;
50
+ }
51
+ if (hasView && !hasBrowsable && dataElements.length === 0) {
52
+ // Plain VIEW with no data and no BROWSABLE — not a deep link, nothing to audit here.
53
+ return;
54
+ }
55
+ const exportedExplicitTrue = component.exported === true;
56
+ const unprotected = !component.permission;
57
+ const scheme = classifyScheme(dataElements);
58
+ const hasHost = dataElements.some((d) => Boolean(d.dataHost));
59
+ const placeholderEvidence = dataElements.some((d) => isPlaceholder(d.dataScheme) || isPlaceholder(d.dataHost) || isPlaceholder(d.dataPath) || isPlaceholder(d.dataPathPrefix) || isPlaceholder(d.dataPathPattern));
60
+ const broadPattern = dataElements.some((d) => (d.dataPathPattern && WILDCARD_PATTERN.test(d.dataPathPattern)) || d.dataPathPattern === ".*" || d.dataPathPattern === "*");
61
+ const evidenceDetails = [
62
+ `scheme=${scheme}`,
63
+ `hasHost=${hasHost}`,
64
+ `exported=${component.exported ?? "unspecified"}`,
65
+ `hasComponentPermission=${!unprotected}`,
66
+ `dataElementCount=${dataElements.length}`,
67
+ ];
68
+ if (placeholderEvidence) {
69
+ findings.push(makeAndroidFinding({
70
+ ruleId: "android-deep-link-placeholder-value",
71
+ title: `Deep-link data on ${kind} "${identity}" contains an unresolved placeholder or resource reference`,
72
+ severity: "informational",
73
+ confidence: "low",
74
+ description: "One or more deep-link data attributes (scheme/host/path) reference an unresolved manifest placeholder (e.g. ${applicationId}) or Android resource (e.g. @string/...). Effective exposure cannot be assessed statically.",
75
+ manifestPath: entry.manifestPath,
76
+ identity,
77
+ location: filter.location,
78
+ evidenceDetails,
79
+ }));
80
+ return;
81
+ }
82
+ if (broadPattern) {
83
+ const strongEvidence = exportedExplicitTrue && unprotected;
84
+ findings.push(makeAndroidFinding({
85
+ ruleId: "android-deep-link-broad-path-pattern",
86
+ title: `Broad/wildcard deep-link path pattern on ${kind} "${identity}"`,
87
+ severity: strongEvidence ? "major" : "minor",
88
+ confidence: strongEvidence ? "high" : "medium",
89
+ description: strongEvidence
90
+ ? "This deep-link filter uses a highly permissive pathPattern, the component is explicitly exported, and no component-level permission protects it — a strong static combination of broad matching and open reachability."
91
+ : "This deep-link filter uses a broad/wildcard-like pathPattern. Broad path matching is not automatically exploitable, but widens the set of URLs the app will handle.",
92
+ manifestPath: entry.manifestPath,
93
+ identity,
94
+ location: filter.location,
95
+ evidenceDetails,
96
+ recommendation: "Narrow the pathPattern to only the routes the app actually needs to handle, and validate any path/query data before use.",
97
+ }));
98
+ return;
99
+ }
100
+ if (scheme === "web" && !hasHost) {
101
+ findings.push(makeAndroidFinding({
102
+ ruleId: "android-deep-link-web-link-without-host",
103
+ title: `HTTP/HTTPS deep link on ${kind} "${identity}" has no host restriction`,
104
+ severity: "minor",
105
+ confidence: "medium",
106
+ description: "This web-link intent filter declares an http/https scheme without a host, so it is not restricted to a specific domain.",
107
+ manifestPath: entry.manifestPath,
108
+ identity,
109
+ location: filter.location,
110
+ evidenceDetails,
111
+ recommendation: "Add android:host to restrict which domains this filter matches.",
112
+ }));
113
+ return;
114
+ }
115
+ if (scheme === "custom" && !hasHost) {
116
+ findings.push(makeAndroidFinding({
117
+ ruleId: "android-deep-link-custom-scheme-without-host",
118
+ title: `Custom-scheme deep link on ${kind} "${identity}" has no host restriction`,
119
+ severity: "minor",
120
+ confidence: "medium",
121
+ description: "This custom-scheme intent filter declares no host restriction. Custom schemes are not automatically vulnerabilities, but an unrestricted scheme accepts any host/path combination.",
122
+ manifestPath: entry.manifestPath,
123
+ identity,
124
+ location: filter.location,
125
+ evidenceDetails,
126
+ recommendation: "Consider restricting the host or validating incoming URIs defensively in the handling code.",
127
+ }));
128
+ return;
129
+ }
130
+ // Bounded/safe deep link: explicit scheme + host (+ optional path
131
+ // restriction), no wildcard pattern, no placeholders — recorded for
132
+ // inventory purposes at informational severity only (ANDROID-B3-24).
133
+ findings.push(makeAndroidFinding({
134
+ ruleId: "android-deep-link-bounded",
135
+ title: `Deep link declared on ${kind} "${identity}"`,
136
+ severity: "informational",
137
+ confidence: "high",
138
+ description: "This deep-link filter declares an explicit scheme and host with bounded path evidence. No static exposure concern was identified.",
139
+ manifestPath: entry.manifestPath,
140
+ identity,
141
+ location: filter.location,
142
+ evidenceDetails,
143
+ }));
144
+ }
145
+ function componentGroups(entry) {
146
+ return [
147
+ { kind: "activity", components: entry.manifest.activities },
148
+ { kind: "activity-alias", components: entry.manifest.activityAliases ?? [] },
149
+ ];
150
+ }
151
+ export function auditAndroidDeepLinks(detection, manifests) {
152
+ const findings = [];
153
+ const evidence = [];
154
+ const warnings = [];
155
+ for (const entry of manifests) {
156
+ let candidateCount = 0;
157
+ for (const group of componentGroups(entry)) {
158
+ for (const component of group.components) {
159
+ for (const filter of component.intentFilters) {
160
+ const actions = filter.actions ?? [];
161
+ const categories = filter.categories ?? [];
162
+ if (actions.includes(VIEW_ACTION) || categories.includes(BROWSABLE_CATEGORY)) {
163
+ candidateCount += 1;
164
+ auditDeepLinkFilter(entry, group.kind, component, filter, findings);
165
+ }
166
+ }
167
+ }
168
+ }
169
+ if (candidateCount > 0) {
170
+ evidence.push(`${entry.manifestPath}: ${candidateCount} deep-link candidate intent-filter(s) inspected`);
171
+ }
172
+ }
173
+ return buildAndroidManifestCheckResult({
174
+ id: "android-deep-links-audit",
175
+ category: "android-deep-links",
176
+ title: "Android deep-link audit",
177
+ detection,
178
+ manifests,
179
+ findings,
180
+ evidence,
181
+ warnings,
182
+ });
183
+ }