@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.
Files changed (116) hide show
  1. package/README.md +46 -270
  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/mobile/android/audit/androidFinding.js +68 -0
  6. package/dist/src/mobile/android/audit/checkResultBuilder.js +97 -0
  7. package/dist/src/mobile/android/audit/deepLinkAudit.js +183 -0
  8. package/dist/src/mobile/android/audit/exportedComponentAudit.js +183 -0
  9. package/dist/src/mobile/android/audit/intentFilterAudit.js +128 -0
  10. package/dist/src/mobile/android/audit/permissionAudit.js +142 -0
  11. package/dist/src/mobile/android/detect/androidTargetDetection.js +18 -0
  12. package/dist/src/mobile/android/detect/buildFileEvidence.js +36 -0
  13. package/dist/src/mobile/android/detect/classify.js +150 -0
  14. package/dist/src/mobile/android/detect/detectAndroidProject.js +252 -0
  15. package/dist/src/mobile/android/detect/settingsModules.js +60 -0
  16. package/dist/src/mobile/android/detect/sourceEvidence.js +92 -0
  17. package/dist/src/mobile/android/detect/traversal.js +79 -0
  18. package/dist/src/mobile/android/detect/versionCatalogEvidence.js +37 -0
  19. package/dist/src/mobile/android/detection.js +28 -0
  20. package/dist/src/mobile/android/gradle/gradleMetadataCheckResult.js +120 -0
  21. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +137 -0
  22. package/dist/src/mobile/android/gradle/pluginVersionExtractor.js +40 -0
  23. package/dist/src/mobile/android/gradle/readAndroidGradleMetadata.js +210 -0
  24. package/dist/src/mobile/android/gradle/releaseMetadataSummary.js +64 -0
  25. package/dist/src/mobile/android/gradle/settingsMetadata.js +20 -0
  26. package/dist/src/mobile/android/gradle/types.js +1 -0
  27. package/dist/src/mobile/android/gradle/validate/executor.js +9 -0
  28. package/dist/src/mobile/android/gradle/validate/operationCheckResult.js +99 -0
  29. package/dist/src/mobile/android/gradle/validate/operations.js +68 -0
  30. package/dist/src/mobile/android/gradle/validate/planner.js +49 -0
  31. package/dist/src/mobile/android/gradle/validate/runOptionalGradleValidation.js +113 -0
  32. package/dist/src/mobile/android/gradle/validate/targetMutation.js +45 -0
  33. package/dist/src/mobile/android/gradle/validate/taskListParser.js +24 -0
  34. package/dist/src/mobile/android/gradle/versionCatalogMetadata.js +94 -0
  35. package/dist/src/mobile/android/gradle/wrapperMetadata.js +32 -0
  36. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +341 -0
  37. package/dist/src/mobile/android/manifest/types.js +29 -0
  38. package/dist/src/mobile/android/manifest/xml/parseXml.js +313 -0
  39. package/dist/src/mobile/android/profile.js +29 -0
  40. package/dist/src/mobile/android/report/model.js +104 -0
  41. package/dist/src/mobile/android/report/renderAndroidReport.js +223 -0
  42. package/dist/src/mobile/android/report/writeAndroidReportFiles.js +23 -0
  43. package/dist/src/mobile/android/targetMetadata.js +27 -0
  44. package/dist/src/mobile/android/validate/androidVerdict.js +248 -0
  45. package/dist/src/mobile/android/validate/detectionAndImmutabilityCheckResults.js +177 -0
  46. package/dist/src/mobile/android/validate/playReadinessChecklist.js +145 -0
  47. package/dist/src/mobile/android/validate/validateAndroidTarget.js +172 -0
  48. package/dist/src/mobile/android/validation/checkResult.js +51 -0
  49. package/dist/src/mobile/android/validation/result.js +25 -0
  50. package/dist/src/mobile/index.js +42 -0
  51. package/dist/src/mobile/types.js +27 -0
  52. package/dist/src/securityValidation/attackScenarios/attackProfile.js +2 -0
  53. package/dist/src/securityValidation/attackScenarios/profiles/androidProfile.js +20 -0
  54. package/dist/src/securityValidation/validate/cliOptions.js +46 -2
  55. package/package.json +2 -1
  56. package/tests/fixtures/android/compose-app/app/build.gradle.kts +21 -0
  57. package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +22 -0
  58. package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +11 -0
  59. package/tests/fixtures/android/compose-app/build.gradle.kts +4 -0
  60. package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  61. package/tests/fixtures/android/compose-app/settings.gradle.kts +2 -0
  62. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +3 -0
  63. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +5 -0
  64. package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +21 -0
  65. package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +17 -0
  66. package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +23 -0
  67. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +4 -0
  68. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +3 -0
  69. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +5 -0
  70. package/tests/fixtures/android/java-app/app/build.gradle +16 -0
  71. package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +19 -0
  72. package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +4 -0
  73. package/tests/fixtures/android/java-app/build.gradle +3 -0
  74. package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  75. package/tests/fixtures/android/java-app/settings.gradle +2 -0
  76. package/tests/fixtures/android/library/build.gradle.kts +3 -0
  77. package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +4 -0
  78. package/tests/fixtures/android/library/mylibrary/build.gradle.kts +13 -0
  79. package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +2 -0
  80. package/tests/fixtures/android/library/settings.gradle.kts +2 -0
  81. package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +14 -0
  82. package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +10 -0
  83. package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +5 -0
  84. package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +7 -0
  85. package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +11 -0
  86. package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +29 -0
  87. package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +62 -0
  88. package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +45 -0
  89. package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +14 -0
  90. package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +11 -0
  91. package/tests/fixtures/android/multi-module/app/build.gradle.kts +22 -0
  92. package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +3 -0
  93. package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +11 -0
  94. package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +3 -0
  95. package/tests/fixtures/android/multi-module/build.gradle.kts +5 -0
  96. package/tests/fixtures/android/multi-module/core/build.gradle.kts +14 -0
  97. package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +2 -0
  98. package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +18 -0
  99. package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +2 -0
  100. package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +8 -0
  101. package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +4 -0
  102. package/tests/fixtures/android/multi-module/settings.gradle.kts +4 -0
  103. package/tests/fixtures/android/non-android-gradle/build.gradle.kts +3 -0
  104. package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +3 -0
  105. package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +4 -0
  106. package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +4 -0
  107. package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +2 -0
  108. package/tests/fixtures/android/partial/app/build.gradle.kts +7 -0
  109. package/tests/fixtures/android/partial/settings.gradle.kts +2 -0
  110. package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +17 -0
  111. package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +31 -0
  112. package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +7 -0
  113. package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +4 -0
  114. package/tests/fixtures/android/xml-view-app/build.gradle.kts +4 -0
  115. package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  116. package/tests/fixtures/android/xml-view-app/settings.gradle.kts +2 -0
@@ -0,0 +1,29 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 1 — Android profile identity.
3
+ //
4
+ // A single canonical profile id ("android") is used for all Android Gradle
5
+ // projects. Application vs. library vs. Compose-vs-XML/View distinctions are
6
+ // represented as classification data on AndroidDetectionResult (see
7
+ // detection.ts), not as separate profile ids — this keeps the public profile
8
+ // surface minimal, per the batch's "no unnecessary user-facing aliases" rule.
9
+ // ---------------------------------------------------------------------------
10
+ export const ANDROID_PLATFORM = "android";
11
+ export const ANDROID_PROFILE_ID = "android";
12
+ export function createAndroidProfile(partial) {
13
+ return {
14
+ id: ANDROID_PROFILE_ID,
15
+ displayName: "Android",
16
+ platform: ANDROID_PLATFORM,
17
+ projectType: partial.projectType,
18
+ projectSubtype: partial.projectSubtype,
19
+ profileVersion: partial.profileVersion,
20
+ detectionConfidence: partial.detectionConfidence,
21
+ evidence: partial.evidence ?? [],
22
+ unsupportedNotes: partial.unsupportedNotes ?? [],
23
+ supportedCapabilities: partial.supportedCapabilities ?? [],
24
+ limitations: partial.limitations ?? [],
25
+ };
26
+ }
27
+ export function isAndroidProfile(profile) {
28
+ return profile.platform === ANDROID_PLATFORM && profile.id === ANDROID_PROFILE_ID;
29
+ }
@@ -0,0 +1,104 @@
1
+ import { androidVerdictToHumanLabel } from "../validate/androidVerdict.js";
2
+ const GRADLE_OPERATION_CHECK_PREFIX = "android-gradle-";
3
+ const GRADLE_METADATA_CHECK_ID = "android-gradle-metadata";
4
+ const STATIC_ANALYSIS_LIMITATIONS = [
5
+ "Static Android project analysis does not prove runtime behavior.",
6
+ "Manifests are parsed independently and are not merged.",
7
+ "Manifest placeholders and resource references are not resolved.",
8
+ "Gradle files are not evaluated (no Groovy/Kotlin execution).",
9
+ "Dynamic Gradle values may remain unresolved.",
10
+ "Same-file duplicate Gradle assignments use first-match extraction (a known limitation — see agents.txt Batch 4/5 notes).",
11
+ "Optional Gradle execution is not performed unless explicitly requested via --android-gradle-operations.",
12
+ "A successful debug build does not prove release build, signing, runtime security, or Play readiness.",
13
+ "Android SDK or dependency availability may limit optional checks.",
14
+ "Digital Asset Links are not verified.",
15
+ "Domain ownership is not verified.",
16
+ "Current Google Play policy is not checked by this tool.",
17
+ "APK/AAB contents and signing are not inspected.",
18
+ ];
19
+ function groupFindingsBySeverity(findings) {
20
+ const grouped = {};
21
+ for (const finding of findings) {
22
+ if (!grouped[finding.severity])
23
+ grouped[finding.severity] = [];
24
+ grouped[finding.severity].push(finding);
25
+ }
26
+ return grouped;
27
+ }
28
+ // Builds the report model from a complete AndroidValidationResult (Batch 5).
29
+ export function toAndroidReportModel(result, options = {}) {
30
+ const permissionCount = result.manifests.reduce((sum, m) => sum + m.permissions.length, 0);
31
+ const componentCount = result.manifests.reduce((sum, m) => sum + m.activities.length + (m.activityAliases?.length ?? 0) + m.services.length + m.receivers.length + m.providers.length, 0);
32
+ const intentFilterCount = result.manifests.reduce((sum, m) => sum + [...m.activities, ...(m.activityAliases ?? []), ...m.services, ...m.receivers].reduce((s, c) => s + c.intentFilters.length, 0), 0);
33
+ const deepLinkCount = result.manifests.reduce((sum, m) => sum + m.deepLinks.length, 0);
34
+ const gradleOperationResults = result.checks.filter((c) => c.id.startsWith(GRADLE_OPERATION_CHECK_PREFIX) && c.id !== GRADLE_METADATA_CHECK_ID);
35
+ const gradleMetadataCheck = result.checks.find((c) => c.id === GRADLE_METADATA_CHECK_ID);
36
+ const verdict = String(result.verdict);
37
+ const isRealVerdict = result.verdict !== "not-calculated";
38
+ return {
39
+ executiveSummary: `Android security-validation summary for ${result.target.local.targetRoot} — ${result.manifests.length} manifest(s), ${result.checks.length} check(s), ${result.findings.length} finding(s).`,
40
+ metadata: {
41
+ tool: result.tool,
42
+ target: result.target,
43
+ profile: options.profile ?? "android",
44
+ requestedGradleOperations: options.requestedGradleOperations ?? [],
45
+ generatedAt: result.finishedAt,
46
+ totalDurationMs: result.durationMs,
47
+ },
48
+ detectionSummary: {
49
+ title: "Detection",
50
+ summary: result.detection.detected
51
+ ? `Detected (confidence: ${result.detection.confidence})`
52
+ : `Not detected (confidence: ${result.detection.confidence})`,
53
+ },
54
+ androidProjectSummary: { title: "Android project classification", summary: result.target.classification.projectKind },
55
+ uiToolkitSummary: { title: "UI toolkit classification", summary: result.target.classification.uiToolkit },
56
+ moduleSummary: result.detection.modules.map((m) => ({ path: m.path, kind: m.kind, uiToolkit: m.uiToolkit })),
57
+ manifestSummary: { title: "Manifests", summary: `${result.manifests.length} manifest(s) parsed independently (not merged)` },
58
+ permissionsSummary: { title: "Permissions", summary: `${permissionCount} declared permission(s) across all manifests` },
59
+ componentSummary: { title: "Components", summary: `${componentCount} component(s) across all manifests` },
60
+ intentFilterSummary: { title: "Intent filters", summary: `${intentFilterCount} intent-filter(s) across all manifests` },
61
+ deepLinkSummary: { title: "Deep links", summary: `${deepLinkCount} deep-link data entr(y/ies) across all manifests` },
62
+ gradleMetadataSummary: {
63
+ title: "Static Gradle metadata",
64
+ summary: `${result.gradle.modules.length} module(s); confidence: ${result.gradle.metadataConfidence}`,
65
+ },
66
+ gradleCheckSummary: gradleMetadataCheck ? { title: "Gradle metadata check", summary: `status: ${gradleMetadataCheck.status}` } : undefined,
67
+ gradleOperationResults,
68
+ releaseMetadataSummary: {
69
+ title: "Release metadata",
70
+ summary: result.releaseMetadata
71
+ ? result.releaseMetadata.applicationModuleSelectionNote
72
+ : result.releaseMetadataSummary.note,
73
+ },
74
+ playReadinessSummary: {
75
+ title: "Play readiness",
76
+ summary: result.playReadiness ? result.playReadiness.note : result.playReadinessSummary.note,
77
+ },
78
+ playReadinessItems: result.playReadiness?.items.map((i) => ({ id: i.id, title: i.title, status: i.status, detail: i.detail })) ?? [],
79
+ findingsBySeverity: groupFindingsBySeverity(result.findings),
80
+ checks: result.checks,
81
+ skippedChecks: result.skippedChecks,
82
+ environmentLimitations: result.target.environmentCapabilities.notes,
83
+ targetMutationSummary: {
84
+ title: "Target mutation evidence",
85
+ summary: result.targetMutationEvidence
86
+ ? result.targetMutationEvidence.comparable
87
+ ? `${result.targetMutationEvidence.unexpectedChanges.length} unexpected change(s), ${result.targetMutationEvidence.expectedGeneratedChanges.length} expected generated output(s)`
88
+ : `unavailable: ${result.targetMutationEvidence.reason ?? "unknown reason"}`
89
+ : "not captured",
90
+ },
91
+ staticAnalysisLimitations: STATIC_ANALYSIS_LIMITATIONS,
92
+ verdict,
93
+ verdictHumanLabel: isRealVerdict ? androidVerdictToHumanLabel(result.verdict) : "not calculated",
94
+ verdictReasons: result.verdictReasons ?? [],
95
+ recommendedNextStep: result.recommendedNextStep ?? "",
96
+ linkedArtifactReferences: result.reportReferences,
97
+ };
98
+ }
99
+ // Minimal deterministic JSON serializer — object literals above are built
100
+ // with a fixed key order, so equivalent inputs always serialize identically
101
+ // (JSON.stringify preserves string-key insertion order).
102
+ export function serializeAndroidReportModel(model) {
103
+ return JSON.stringify(model, null, 2);
104
+ }
@@ -0,0 +1,223 @@
1
+ import { stripUnsafeControlChars } from "../../../securityValidation/attackScenarios/exploitEvidence.js";
2
+ // ---------------------------------------------------------------------------
3
+ // v0.4.0 Batch 5 — Android text report rendering (agents.txt Batch 5 section
4
+ // 15.4). Mirrors the section structure and defensive text-sanitization
5
+ // convention of src/securityValidation/report/renderSecurityReport.ts
6
+ // (stripUnsafeControlChars guards against ANSI/control-byte report
7
+ // poisoning from scanned manifest/Gradle content).
8
+ // ---------------------------------------------------------------------------
9
+ function divider(char = "-", width = 72) {
10
+ return char.repeat(width);
11
+ }
12
+ function sanitize(raw) {
13
+ return stripUnsafeControlChars(raw);
14
+ }
15
+ function formatDuration(ms) {
16
+ if (ms < 1000)
17
+ return `${ms}ms`;
18
+ return `${(ms / 1000).toFixed(1)}s`;
19
+ }
20
+ export function renderAndroidTextReport(model) {
21
+ const lines = [];
22
+ // 1. Summary
23
+ lines.push(divider("="));
24
+ lines.push("ANDROID SECURITY-VALIDATION SUMMARY");
25
+ lines.push(divider("="));
26
+ lines.push(sanitize(model.executiveSummary));
27
+ lines.push(`Profile : ${model.metadata.profile}`);
28
+ lines.push(`Target : ${model.metadata.target.local.targetRoot}`);
29
+ lines.push(`Tool : ${model.metadata.tool.toolPackageName}@${model.metadata.tool.toolPackageVersion}`);
30
+ lines.push(`Generated : ${model.metadata.generatedAt}`);
31
+ lines.push(`Duration : ${formatDuration(model.metadata.totalDurationMs)}`);
32
+ lines.push("");
33
+ // 2-3. Verdict and reasons
34
+ lines.push(divider());
35
+ lines.push("VERDICT");
36
+ lines.push(divider());
37
+ lines.push(`Verdict: ${model.verdict} (${model.verdictHumanLabel})`);
38
+ lines.push(`Next step: ${sanitize(model.recommendedNextStep)}`);
39
+ if (model.verdictReasons.length > 0) {
40
+ lines.push("");
41
+ lines.push("Verdict reasons:");
42
+ for (const r of model.verdictReasons) {
43
+ lines.push(` [${r.impact}] ${r.code}: ${sanitize(r.summary)}`);
44
+ lines.push(` -> ${sanitize(r.recommendedAction)}`);
45
+ }
46
+ }
47
+ lines.push("");
48
+ // 4. Target identity
49
+ lines.push(divider());
50
+ lines.push("TARGET IDENTITY");
51
+ lines.push(divider());
52
+ lines.push(`Target root : ${model.metadata.target.local.targetRoot}`);
53
+ lines.push(`Tool root : ${model.metadata.target.local.toolRoot}`);
54
+ lines.push(`Package : ${model.metadata.target.local.packageName ?? "(unknown)"}`);
55
+ lines.push(`Git branch : ${model.metadata.target.local.branch ?? "(unavailable)"}`);
56
+ lines.push(`Git commit : ${model.metadata.target.local.commit ?? "(unavailable)"}`);
57
+ lines.push("");
58
+ // 5-6. Classification
59
+ lines.push(divider());
60
+ lines.push("ANDROID PROJECT CLASSIFICATION");
61
+ lines.push(divider());
62
+ lines.push(`${model.detectionSummary.title}: ${model.detectionSummary.summary}`);
63
+ lines.push(`${model.androidProjectSummary.title}: ${model.androidProjectSummary.summary}`);
64
+ lines.push(`${model.uiToolkitSummary.title}: ${model.uiToolkitSummary.summary}`);
65
+ lines.push("");
66
+ // 7. Module summary
67
+ lines.push(divider());
68
+ lines.push("MODULE SUMMARY");
69
+ lines.push(divider());
70
+ if (model.moduleSummary.length === 0) {
71
+ lines.push("(no modules detected)");
72
+ }
73
+ else {
74
+ for (const m of model.moduleSummary) {
75
+ lines.push(` ${m.path.padEnd(30)} kind=${m.kind.padEnd(12)} uiToolkit=${m.uiToolkit ?? "uncertain"}`);
76
+ }
77
+ }
78
+ lines.push("");
79
+ // 8. Environment and execution summary
80
+ lines.push(divider());
81
+ lines.push("ENVIRONMENT AND EXECUTION SUMMARY");
82
+ lines.push(divider());
83
+ lines.push(`Requested Gradle operations: ${model.metadata.requestedGradleOperations.length > 0 ? model.metadata.requestedGradleOperations.join(", ") : "(none — static-only validation, no Gradle process executed)"}`);
84
+ if (model.environmentLimitations.length > 0) {
85
+ for (const note of model.environmentLimitations)
86
+ lines.push(` - ${sanitize(note)}`);
87
+ }
88
+ lines.push("");
89
+ // 9-13. Manifest / permissions / components / intent-filters / deep-links
90
+ lines.push(divider());
91
+ lines.push("MANIFEST SUMMARY");
92
+ lines.push(divider());
93
+ lines.push(sanitize(model.manifestSummary.summary));
94
+ lines.push("");
95
+ lines.push(divider());
96
+ lines.push("PERMISSIONS SUMMARY");
97
+ lines.push(divider());
98
+ lines.push(sanitize(model.permissionsSummary.summary));
99
+ lines.push("");
100
+ lines.push(divider());
101
+ lines.push("EXPORTED COMPONENT SUMMARY");
102
+ lines.push(divider());
103
+ lines.push(sanitize(model.componentSummary.summary));
104
+ lines.push("");
105
+ lines.push(divider());
106
+ lines.push("INTENT-FILTER SUMMARY");
107
+ lines.push(divider());
108
+ lines.push(sanitize(model.intentFilterSummary.summary));
109
+ lines.push("");
110
+ lines.push(divider());
111
+ lines.push("DEEP-LINK SUMMARY");
112
+ lines.push(divider());
113
+ lines.push(sanitize(model.deepLinkSummary.summary));
114
+ lines.push("");
115
+ // 14. Static Gradle metadata
116
+ lines.push(divider());
117
+ lines.push("STATIC GRADLE METADATA");
118
+ lines.push(divider());
119
+ lines.push(sanitize(model.gradleMetadataSummary.summary));
120
+ if (model.gradleCheckSummary)
121
+ lines.push(sanitize(model.gradleCheckSummary.summary));
122
+ lines.push("");
123
+ // 15. Optional Gradle operation results
124
+ lines.push(divider());
125
+ lines.push("OPTIONAL GRADLE OPERATION RESULTS");
126
+ lines.push(divider());
127
+ if (model.gradleOperationResults.length === 0) {
128
+ lines.push("No optional Gradle operations were requested — zero Gradle processes executed.");
129
+ }
130
+ else {
131
+ for (const op of model.gradleOperationResults) {
132
+ lines.push(` ${op.id} — status=${op.status} ran=${op.ran}`);
133
+ if (op.command) {
134
+ lines.push(` command: ${sanitize(op.command.command)} ${op.command.args.map(sanitize).join(" ")}`);
135
+ lines.push(` cwd: ${op.command.cwd} exitCode: ${op.command.exitCode ?? "null"} timedOut: ${op.command.timedOut}`);
136
+ }
137
+ if (op.durationMs !== undefined)
138
+ lines.push(` duration: ${formatDuration(op.durationMs)}`);
139
+ if (op.skipInfo)
140
+ lines.push(` skip reason: ${sanitize(op.skipInfo.reason)}`);
141
+ for (const w of op.warnings)
142
+ lines.push(` warning: ${sanitize(w)}`);
143
+ }
144
+ }
145
+ lines.push("");
146
+ // 16. Release metadata
147
+ lines.push(divider());
148
+ lines.push("RELEASE METADATA");
149
+ lines.push(divider());
150
+ lines.push(sanitize(model.releaseMetadataSummary.summary));
151
+ lines.push("");
152
+ // 17. Play-readiness checklist
153
+ lines.push(divider());
154
+ lines.push("PLAY-READINESS CHECKLIST PLACEHOLDERS");
155
+ lines.push(divider());
156
+ lines.push(sanitize(model.playReadinessSummary.summary));
157
+ for (const item of model.playReadinessItems) {
158
+ lines.push(` [${item.status}] ${item.title}: ${sanitize(item.detail)}`);
159
+ }
160
+ lines.push("");
161
+ // 18. Findings grouped by severity
162
+ lines.push(divider());
163
+ lines.push("FINDINGS BY SEVERITY");
164
+ lines.push(divider());
165
+ const severityOrder = ["blocker", "major", "minor", "informational"];
166
+ let anyFindings = false;
167
+ for (const severity of severityOrder) {
168
+ const findings = model.findingsBySeverity[severity];
169
+ if (!findings || findings.length === 0)
170
+ continue;
171
+ anyFindings = true;
172
+ lines.push(`${severity.toUpperCase()} (${findings.length}):`);
173
+ for (const f of findings) {
174
+ lines.push(` [${f.id}] ${sanitize(f.title)}`);
175
+ lines.push(` category: ${f.category}`);
176
+ if (f.affectedFiles && f.affectedFiles.length > 0)
177
+ lines.push(` path: ${f.affectedFiles.join(", ")}`);
178
+ lines.push(` evidence: ${sanitize(f.evidence ?? "(none)")}`);
179
+ lines.push(` ${sanitize(f.description)}`);
180
+ if (f.recommendation)
181
+ lines.push(` recommendation: ${sanitize(f.recommendation)}`);
182
+ }
183
+ }
184
+ if (!anyFindings)
185
+ lines.push("(no findings)");
186
+ lines.push("");
187
+ // 19. Skipped/unsupported/not-run/inconclusive checks
188
+ lines.push(divider());
189
+ lines.push("SKIPPED, UNSUPPORTED, NOT-RUN, AND INCONCLUSIVE CHECKS");
190
+ lines.push(divider());
191
+ const nonPassing = model.checks.filter((c) => c.status !== "passed");
192
+ if (nonPassing.length === 0) {
193
+ lines.push("(all checks passed)");
194
+ }
195
+ else {
196
+ for (const c of nonPassing) {
197
+ lines.push(` [${c.status}] ${c.id} — ${sanitize(c.title)}`);
198
+ if (c.skipInfo)
199
+ lines.push(` reason: ${sanitize(c.skipInfo.reason)}`);
200
+ }
201
+ }
202
+ lines.push("");
203
+ // 20. Target mutation evidence
204
+ lines.push(divider());
205
+ lines.push("TARGET MUTATION EVIDENCE");
206
+ lines.push(divider());
207
+ lines.push(sanitize(model.targetMutationSummary.summary));
208
+ lines.push("");
209
+ // 21. Static-analysis limitations
210
+ lines.push(divider());
211
+ lines.push("STATIC-ANALYSIS LIMITATIONS");
212
+ lines.push(divider());
213
+ for (const limitation of model.staticAnalysisLimitations) {
214
+ lines.push(` - ${limitation}`);
215
+ }
216
+ lines.push("");
217
+ // 22. Recommended next step
218
+ lines.push(divider());
219
+ lines.push("RECOMMENDED NEXT STEP");
220
+ lines.push(divider());
221
+ lines.push(sanitize(model.recommendedNextStep));
222
+ return lines.join("\n");
223
+ }
@@ -0,0 +1,23 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { renderAndroidTextReport } from "./renderAndroidReport.js";
4
+ import { serializeAndroidReportModel } from "./model.js";
5
+ export function writeAndroidReportFiles(options) {
6
+ const reportDir = options.outDir;
7
+ if (!fs.existsSync(reportDir)) {
8
+ fs.mkdirSync(reportDir, { recursive: true });
9
+ }
10
+ const basename = `${options.prefix}-android-security-validation`;
11
+ const textPath = options.formats.includes("text") ? path.join(reportDir, `${basename}.txt`) : null;
12
+ const jsonPath = options.formats.includes("json") ? path.join(reportDir, `${basename}.json`) : null;
13
+ const writtenPaths = [];
14
+ if (textPath) {
15
+ fs.writeFileSync(textPath, renderAndroidTextReport(options.report), "utf8");
16
+ writtenPaths.push(textPath);
17
+ }
18
+ if (jsonPath) {
19
+ fs.writeFileSync(jsonPath, serializeAndroidReportModel(options.report), "utf8");
20
+ writtenPaths.push(jsonPath);
21
+ }
22
+ return { textPath, jsonPath, writtenPaths };
23
+ }
@@ -0,0 +1,27 @@
1
+ export function createAndroidTargetMetadata(partial) {
2
+ const { local, androidProfile, detection } = partial;
3
+ return {
4
+ local,
5
+ androidProfile,
6
+ classification: {
7
+ projectKind: detection.projectKind,
8
+ uiToolkit: detection.uiToolkit,
9
+ },
10
+ modules: detection.modules.map((module) => module.path),
11
+ applicationModules: detection.applicationModules,
12
+ libraryModules: detection.libraryModules,
13
+ manifestPaths: detection.manifestPaths,
14
+ gradleWrapper: partial.gradleWrapper ?? { present: detection.hasGradleWrapper },
15
+ releaseMetadata: partial.releaseMetadata ?? {
16
+ available: false,
17
+ note: "Release metadata summary is not implemented in this batch.",
18
+ },
19
+ environmentCapabilities: partial.environmentCapabilities ?? {
20
+ gradleAvailable: "unknown",
21
+ androidSdkAvailable: "unknown",
22
+ notes: ["Environment capability detection is not implemented in this batch."],
23
+ },
24
+ detectionConfidence: detection.confidence,
25
+ detectionEvidence: detection.evidence,
26
+ };
27
+ }
@@ -0,0 +1,248 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 5 — initial Android verdict policy (agents.txt Batch 5
3
+ // section 12).
4
+ //
5
+ // Reuses the existing security-validation ReleaseVerdict vocabulary (all
6
+ // four machine values match or map directly — see result.ts's
7
+ // AndroidVerdictValue comment) rather than a parallel Android-only
8
+ // vocabulary. Precedence, evaluated in order, first match wins:
9
+ // 1. not-ready-security-blocker-remains
10
+ // 2. inconclusive-audit-environment-incomplete
11
+ // 3. ready-except-optional-manual-checks
12
+ // 4. ready-for-release-preparation
13
+ // A stronger negative verdict is never overwritten by a weaker positive one.
14
+ // ---------------------------------------------------------------------------
15
+ const AUDIT_FINDING_DRIVEN_CATEGORIES = new Set(["android-permissions", "android-components", "android-intent-filters", "android-deep-links"]);
16
+ const REQUIRED_CHECK_CATEGORIES = new Set([
17
+ "android-detection",
18
+ "android-manifest",
19
+ "android-permissions",
20
+ "android-components",
21
+ "android-intent-filters",
22
+ "android-deep-links",
23
+ "android-gradle",
24
+ ]);
25
+ export function androidVerdictToHumanLabel(verdict) {
26
+ switch (verdict) {
27
+ case "ready-for-release-preparation":
28
+ return "ready for release preparation";
29
+ case "not-ready-security-blocker-remains":
30
+ return "not ready: security blocker remains";
31
+ case "ready-except-optional-manual-checks":
32
+ return "ready except optional manual checks";
33
+ case "inconclusive-audit-environment-incomplete":
34
+ return "inconclusive: android environment incomplete";
35
+ }
36
+ }
37
+ export function calculateAndroidVerdict(input) {
38
+ const { checks, findings, detection, playReadiness, targetMutation, multipleApplicationModules } = input;
39
+ const reasons = [];
40
+ const blockerFindings = findings.filter((f) => f.severity === "blocker");
41
+ const majorFindings = findings.filter((f) => f.severity === "major");
42
+ const minorOrInformationalFindings = findings.filter((f) => f.severity === "minor" || f.severity === "informational");
43
+ // Filtered by requirementLevel as well as category: optional Gradle
44
+ // operation checks (e.g. android-gradle-unit-test-debug) share the
45
+ // "android-gradle" category with the required static metadata check
46
+ // (android-gradle-metadata) but must never be treated as a required check
47
+ // — an optional operation's failure belongs in the advisory tier (section
48
+ // 12.5), not the not-ready tier.
49
+ const requiredChecks = checks.filter((c) => REQUIRED_CHECK_CATEGORIES.has(c.category) && c.requirementLevel === "required");
50
+ // Audit-family checks (permissions/components/intent-filters/deep-links)
51
+ // report status "failed" whenever ANY non-informational finding exists,
52
+ // regardless of severity (see audit/checkResultBuilder.ts) — that binary
53
+ // status is deliberately NOT used for the not-ready decision here, since a
54
+ // single minor finding would otherwise be indistinguishable from a
55
+ // blocker. Finding *severity* (checked separately above/below) is the
56
+ // authoritative signal for those four categories; a "failed" status only
57
+ // blocks the verdict for the non-audit required checks (detection,
58
+ // manifest parsing, Gradle metadata), where it reflects a genuine
59
+ // orchestration/parsing failure rather than a graded finding.
60
+ const failedRequired = requiredChecks.filter((c) => c.status === "failed" && !AUDIT_FINDING_DRIVEN_CATEGORIES.has(c.category));
61
+ const inconclusiveRequired = requiredChecks.filter((c) => c.status === "inconclusive" || c.status === "error");
62
+ const unsupportedRequired = requiredChecks.filter((c) => c.status === "unsupported");
63
+ const skippedOptional = checks.filter((c) => c.requirementLevel === "optional" && (c.status === "skipped" || c.status === "not-run"));
64
+ const failedOptional = checks.filter((c) => c.requirementLevel === "optional" && c.status === "failed");
65
+ // ---- 1. not-ready-security-blocker-remains ---------------------------------
66
+ if (blockerFindings.length > 0) {
67
+ for (const f of blockerFindings) {
68
+ reasons.push({
69
+ code: "android-blocker-finding",
70
+ summary: `Blocker-severity finding: ${f.title}`,
71
+ relatedFindingIds: [f.id],
72
+ severityOrStatus: "blocker",
73
+ impact: "blocking",
74
+ recommendedAction: f.recommendation ?? "Resolve this finding before release preparation.",
75
+ });
76
+ }
77
+ return { verdict: "not-ready-security-blocker-remains", recommendedNextStep: `Fix ${blockerFindings.length} blocker finding(s) before release.`, reasons };
78
+ }
79
+ if (majorFindings.length > 0) {
80
+ for (const f of majorFindings) {
81
+ reasons.push({
82
+ code: "android-major-finding",
83
+ summary: `Major-severity finding: ${f.title}`,
84
+ relatedFindingIds: [f.id],
85
+ severityOrStatus: "major",
86
+ impact: "blocking",
87
+ recommendedAction: f.recommendation ?? "Resolve or explicitly accept this finding before release preparation.",
88
+ });
89
+ }
90
+ return { verdict: "not-ready-security-blocker-remains", recommendedNextStep: `Review ${majorFindings.length} major finding(s) before release.`, reasons };
91
+ }
92
+ if (targetMutation.comparable && targetMutation.unexpectedChanges.length > 0) {
93
+ reasons.push({
94
+ code: "android-unexpected-target-modification",
95
+ summary: `Unexpected target change(s) observed: ${targetMutation.unexpectedChanges.join(", ")}`,
96
+ severityOrStatus: "blocking",
97
+ impact: "blocking",
98
+ recommendedAction: "Investigate why validation modified tracked target files and re-run.",
99
+ });
100
+ return { verdict: "not-ready-security-blocker-remains", recommendedNextStep: "An unexpected tracked target-file modification occurred during validation. Investigate before release.", reasons };
101
+ }
102
+ if (failedRequired.length > 0) {
103
+ for (const c of failedRequired) {
104
+ reasons.push({
105
+ code: "android-required-check-failed",
106
+ summary: `Required check failed: ${c.title}`,
107
+ relatedCheckId: c.id,
108
+ severityOrStatus: c.status,
109
+ impact: "blocking",
110
+ recommendedAction: "Investigate the failure evidence for this check.",
111
+ });
112
+ }
113
+ return { verdict: "not-ready-security-blocker-remains", recommendedNextStep: `Required check(s) failed: ${failedRequired.map((c) => c.title).join(", ")}.`, reasons };
114
+ }
115
+ // ---- 2. inconclusive-audit-environment-incomplete ---------------------------
116
+ // Profile mismatch: the target was not detected as Android at all. This
117
+ // must never read as "ready" — every required check above reports
118
+ // "unsupported", not "passed" (agents.txt Batch 5 section 9.3).
119
+ if (detection.projectKind === "non-android") {
120
+ reasons.push({
121
+ code: "android-profile-mismatch",
122
+ summary: "The --profile android target was not detected as an Android Gradle project.",
123
+ impact: "inconclusive",
124
+ recommendedAction: "Re-run against an Android Gradle project, or select a different --profile for this target.",
125
+ });
126
+ return {
127
+ verdict: "inconclusive-audit-environment-incomplete",
128
+ recommendedNextStep: "This target does not appear to be an Android Gradle project. Android validation could not run.",
129
+ reasons,
130
+ };
131
+ }
132
+ if (detection.projectKind === "partial" || detection.projectKind === "unknown") {
133
+ reasons.push({
134
+ code: "android-partial-detection",
135
+ summary: `Android project detection is partial or ambiguous (projectKind=${detection.projectKind}).`,
136
+ severityOrStatus: detection.confidence,
137
+ impact: "inconclusive",
138
+ recommendedAction: "Confirm the target is a complete Android Gradle project and re-run detection.",
139
+ });
140
+ }
141
+ if (inconclusiveRequired.length > 0) {
142
+ for (const c of inconclusiveRequired) {
143
+ reasons.push({
144
+ code: "android-required-check-inconclusive",
145
+ summary: `Required check is ${c.status}: ${c.title}`,
146
+ relatedCheckId: c.id,
147
+ severityOrStatus: c.status,
148
+ impact: "inconclusive",
149
+ recommendedAction: c.skipInfo?.recommendedNextAction ?? "Provide the missing evidence and re-run.",
150
+ });
151
+ }
152
+ }
153
+ if (unsupportedRequired.length > 0) {
154
+ for (const c of unsupportedRequired) {
155
+ reasons.push({
156
+ code: "android-required-check-unsupported",
157
+ summary: `Required check is unsupported: ${c.title}`,
158
+ relatedCheckId: c.id,
159
+ severityOrStatus: c.status,
160
+ impact: "inconclusive",
161
+ recommendedAction: c.skipInfo?.recommendedNextAction ?? "Provide the missing capability and re-run.",
162
+ });
163
+ }
164
+ }
165
+ if (multipleApplicationModules) {
166
+ reasons.push({
167
+ code: "android-multiple-application-modules",
168
+ summary: "Multiple application modules were detected; a single release target could not be selected.",
169
+ impact: "inconclusive",
170
+ recommendedAction: "Validate each application module individually, or confirm which module is the release target.",
171
+ });
172
+ }
173
+ if ((detection.projectKind === "partial" || detection.projectKind === "unknown") ||
174
+ inconclusiveRequired.length > 0 ||
175
+ unsupportedRequired.length > 0 ||
176
+ multipleApplicationModules) {
177
+ return {
178
+ verdict: "inconclusive-audit-environment-incomplete",
179
+ recommendedNextStep: "Required Android evidence is incomplete. Address the reasons above and re-run android validation.",
180
+ reasons,
181
+ };
182
+ }
183
+ // ---- 3. ready-except-optional-manual-checks ---------------------------------
184
+ if (minorOrInformationalFindings.length > 0) {
185
+ for (const f of minorOrInformationalFindings) {
186
+ reasons.push({
187
+ code: "android-review-finding",
188
+ summary: `${f.severity} finding: ${f.title}`,
189
+ relatedFindingIds: [f.id],
190
+ severityOrStatus: f.severity,
191
+ impact: "advisory",
192
+ recommendedAction: f.recommendation ?? "Review before release.",
193
+ });
194
+ }
195
+ }
196
+ if (skippedOptional.length > 0) {
197
+ for (const c of skippedOptional) {
198
+ reasons.push({
199
+ code: "android-optional-check-not-run",
200
+ summary: `Optional check was not run or skipped: ${c.title}`,
201
+ relatedCheckId: c.id,
202
+ severityOrStatus: c.status,
203
+ impact: "advisory",
204
+ recommendedAction: "Run explicitly via --android-gradle-operations if this evidence is needed.",
205
+ });
206
+ }
207
+ }
208
+ if (failedOptional.length > 0) {
209
+ for (const c of failedOptional) {
210
+ reasons.push({
211
+ code: "android-optional-check-failed",
212
+ summary: `Optional operation failed: ${c.title}`,
213
+ relatedCheckId: c.id,
214
+ severityOrStatus: c.status,
215
+ impact: "advisory",
216
+ recommendedAction: "Review the operation output; an optional build/test/lint failure is validation evidence, not a security finding.",
217
+ });
218
+ }
219
+ }
220
+ const manualPlayItems = playReadiness.items.filter((i) => i.status === "manual-check-required" || i.status === "unresolved" || i.status === "missing");
221
+ if (playReadiness.applicable && manualPlayItems.length > 0) {
222
+ reasons.push({
223
+ code: "android-play-readiness-manual-items",
224
+ summary: `${manualPlayItems.length} Play-readiness checklist item(s) require manual confirmation.`,
225
+ impact: "advisory",
226
+ recommendedAction: "Review the Play-readiness checklist section before publishing.",
227
+ });
228
+ }
229
+ if (reasons.length > 0) {
230
+ return {
231
+ verdict: "ready-except-optional-manual-checks",
232
+ recommendedNextStep: "No blocking or required-evidence gaps remain. Review the advisory items above (findings, skipped optional checks, and manual Play-readiness items) before release preparation.",
233
+ reasons,
234
+ };
235
+ }
236
+ // ---- 4. ready-for-release-preparation ----------------------------------------
237
+ reasons.push({
238
+ code: "android-ready",
239
+ summary: "All required static Android checks completed with no blocking findings, no unexpected target modification, and no outstanding advisory items.",
240
+ impact: "advisory",
241
+ recommendedAction: "Proceed with release preparation. This does not certify Google Play compliance or publication readiness.",
242
+ });
243
+ return {
244
+ verdict: "ready-for-release-preparation",
245
+ recommendedNextStep: "All required static Android checks passed. Proceed with release preparation (this is not a Google Play compliance or publication-readiness claim).",
246
+ reasons,
247
+ };
248
+ }