@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.
- package/README.md +46 -269
- package/dist/scripts/security/validate.js +177 -70
- package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +5 -2
- package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +5 -2
- package/dist/src/audits/core/collectSourceFacts.js +21 -10
- package/dist/src/mobile/android/audit/androidFinding.js +68 -0
- package/dist/src/mobile/android/audit/checkResultBuilder.js +97 -0
- package/dist/src/mobile/android/audit/deepLinkAudit.js +183 -0
- package/dist/src/mobile/android/audit/exportedComponentAudit.js +183 -0
- package/dist/src/mobile/android/audit/intentFilterAudit.js +128 -0
- package/dist/src/mobile/android/audit/permissionAudit.js +142 -0
- package/dist/src/mobile/android/detect/androidTargetDetection.js +18 -0
- package/dist/src/mobile/android/detect/buildFileEvidence.js +36 -0
- package/dist/src/mobile/android/detect/classify.js +150 -0
- package/dist/src/mobile/android/detect/detectAndroidProject.js +252 -0
- package/dist/src/mobile/android/detect/settingsModules.js +60 -0
- package/dist/src/mobile/android/detect/sourceEvidence.js +92 -0
- package/dist/src/mobile/android/detect/traversal.js +79 -0
- package/dist/src/mobile/android/detect/versionCatalogEvidence.js +37 -0
- package/dist/src/mobile/android/detection.js +28 -0
- package/dist/src/mobile/android/gradle/gradleMetadataCheckResult.js +120 -0
- package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +137 -0
- package/dist/src/mobile/android/gradle/pluginVersionExtractor.js +40 -0
- package/dist/src/mobile/android/gradle/readAndroidGradleMetadata.js +210 -0
- package/dist/src/mobile/android/gradle/releaseMetadataSummary.js +64 -0
- package/dist/src/mobile/android/gradle/settingsMetadata.js +20 -0
- package/dist/src/mobile/android/gradle/types.js +1 -0
- package/dist/src/mobile/android/gradle/validate/executor.js +9 -0
- package/dist/src/mobile/android/gradle/validate/operationCheckResult.js +99 -0
- package/dist/src/mobile/android/gradle/validate/operations.js +68 -0
- package/dist/src/mobile/android/gradle/validate/planner.js +49 -0
- package/dist/src/mobile/android/gradle/validate/runOptionalGradleValidation.js +113 -0
- package/dist/src/mobile/android/gradle/validate/targetMutation.js +45 -0
- package/dist/src/mobile/android/gradle/validate/taskListParser.js +24 -0
- package/dist/src/mobile/android/gradle/versionCatalogMetadata.js +94 -0
- package/dist/src/mobile/android/gradle/wrapperMetadata.js +32 -0
- package/dist/src/mobile/android/manifest/parseAndroidManifest.js +341 -0
- package/dist/src/mobile/android/manifest/types.js +29 -0
- package/dist/src/mobile/android/manifest/xml/parseXml.js +313 -0
- package/dist/src/mobile/android/profile.js +29 -0
- package/dist/src/mobile/android/report/model.js +104 -0
- package/dist/src/mobile/android/report/renderAndroidReport.js +223 -0
- package/dist/src/mobile/android/report/writeAndroidReportFiles.js +23 -0
- package/dist/src/mobile/android/targetMetadata.js +27 -0
- package/dist/src/mobile/android/validate/androidVerdict.js +248 -0
- package/dist/src/mobile/android/validate/detectionAndImmutabilityCheckResults.js +177 -0
- package/dist/src/mobile/android/validate/playReadinessChecklist.js +145 -0
- package/dist/src/mobile/android/validate/validateAndroidTarget.js +172 -0
- package/dist/src/mobile/android/validation/checkResult.js +51 -0
- package/dist/src/mobile/android/validation/result.js +25 -0
- package/dist/src/mobile/index.js +42 -0
- package/dist/src/mobile/types.js +27 -0
- package/dist/src/securityValidation/attackScenarios/attackProfile.js +2 -0
- package/dist/src/securityValidation/attackScenarios/profiles/androidProfile.js +20 -0
- package/dist/src/securityValidation/validate/cliOptions.js +46 -2
- package/package.json +2 -1
- package/tests/fixtures/android/compose-app/app/build.gradle.kts +21 -0
- package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +22 -0
- package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +11 -0
- package/tests/fixtures/android/compose-app/build.gradle.kts +4 -0
- package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/compose-app/settings.gradle.kts +2 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +3 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +5 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +21 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +17 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +23 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +4 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +3 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +5 -0
- package/tests/fixtures/android/java-app/app/build.gradle +16 -0
- package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +19 -0
- package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +4 -0
- package/tests/fixtures/android/java-app/build.gradle +3 -0
- package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/java-app/settings.gradle +2 -0
- package/tests/fixtures/android/library/build.gradle.kts +3 -0
- package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/library/mylibrary/build.gradle.kts +13 -0
- package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/library/settings.gradle.kts +2 -0
- package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +14 -0
- package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +10 -0
- package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +5 -0
- package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +7 -0
- package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +11 -0
- package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +29 -0
- package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +62 -0
- package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +45 -0
- package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +14 -0
- package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +11 -0
- package/tests/fixtures/android/multi-module/app/build.gradle.kts +22 -0
- package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +3 -0
- package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +11 -0
- package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +3 -0
- package/tests/fixtures/android/multi-module/build.gradle.kts +5 -0
- package/tests/fixtures/android/multi-module/core/build.gradle.kts +14 -0
- package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +18 -0
- package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +8 -0
- package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/multi-module/settings.gradle.kts +4 -0
- package/tests/fixtures/android/non-android-gradle/build.gradle.kts +3 -0
- package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +3 -0
- package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +4 -0
- package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +2 -0
- package/tests/fixtures/android/partial/app/build.gradle.kts +7 -0
- package/tests/fixtures/android/partial/settings.gradle.kts +2 -0
- package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +17 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +31 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +7 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +4 -0
- package/tests/fixtures/android/xml-view-app/build.gradle.kts +4 -0
- package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/xml-view-app/settings.gradle.kts +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/README.md +16 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/build.gradle.kts +4 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/java/com/example/Widget.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/kotlin/com/example/Gadget.kt +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/build/Main.class +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/dist/index.js +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/generated/schema.ts +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/vendor/lib.py +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/build.gradle.kts +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/settings.gradle.kts +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/java/com/example/FooService.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/kotlin/com/example/BarService.kt +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/java/com/example/FooServiceTest.java +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/kotlin/com/example/BarServiceTest.kt +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/README.md +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/app.py +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/tests/test_app.py +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/README.md +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/helper.js +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/tests/index.sample.ts +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/build.gradle.kts +4 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/pytests/test_app.py +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/app.py +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/helper.js +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/java/com/example/Multiplier.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/kotlin/com/example/Divider.kt +7 -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
|
+
}
|