@dailephd/my-dev-kit-lab 0.3.4 → 0.4.1
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 +12 -287
- package/dist/scripts/security/validate.js +210 -70
- package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +5 -2
- package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +5 -2
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
- package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
- package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
- package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
- package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
- package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
- package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
- package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
- package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
- package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
- package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
- 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 +243 -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 +364 -0
- package/dist/src/mobile/android/manifest/types.js +29 -0
- package/dist/src/mobile/android/manifest/xml/parseXml.js +329 -0
- package/dist/src/mobile/android/profile.js +29 -0
- package/dist/src/mobile/android/report/model.js +135 -0
- package/dist/src/mobile/android/report/renderAndroidReport.js +286 -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 +315 -0
- package/dist/src/mobile/android/validation/checkResult.js +58 -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/commandRunner.js +10 -1
- package/dist/src/securityValidation/validate/cliOptions.js +92 -2
- package/package.json +2 -1
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
- package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
- package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
- 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/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { findChildren, parseXmlDocument } from "../../manifest/xml/parseXml.js";
|
|
2
|
+
import { MAX_DOMAIN_CONFIG_DEPTH, } from "./types.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.1 Batch 2 — dependency-free Network Security Config XML parser.
|
|
5
|
+
//
|
|
6
|
+
// Builds on the existing bounded XML tree parser
|
|
7
|
+
// (src/mobile/android/manifest/xml/parseXml.ts) rather than creating a
|
|
8
|
+
// second tokenizer — this file only adds NSC-specific semantic parsing on
|
|
9
|
+
// top of the generic XmlElement tree, per agents.txt Batch 2 section 12.
|
|
10
|
+
//
|
|
11
|
+
// Parses exactly one already-resolved, target-contained file. Never merges
|
|
12
|
+
// multiple files, never evaluates resource overlays, never invokes Gradle/
|
|
13
|
+
// Android SDK/network. Malformed XML or an unsupported root never throws —
|
|
14
|
+
// both map to a structured, non-"parsed" result state.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const MAX_PIN_VALUE_PREVIEW_LENGTH = 128;
|
|
17
|
+
const SUPPORTED_PIN_DIGESTS = new Set(["SHA-256"]);
|
|
18
|
+
function getUnprefixedAttribute(element, localName) {
|
|
19
|
+
return element.attributes.find((attr) => attr.prefix === undefined && attr.localName === localName)?.value;
|
|
20
|
+
}
|
|
21
|
+
function parseBooleanAttribute(element, localName) {
|
|
22
|
+
const raw = getUnprefixedAttribute(element, localName);
|
|
23
|
+
if (raw === undefined)
|
|
24
|
+
return {};
|
|
25
|
+
if (raw === "true")
|
|
26
|
+
return { value: true, raw };
|
|
27
|
+
if (raw === "false")
|
|
28
|
+
return { value: false, raw };
|
|
29
|
+
return { raw };
|
|
30
|
+
}
|
|
31
|
+
function boundedPreview(value, maxLength = MAX_PIN_VALUE_PREVIEW_LENGTH) {
|
|
32
|
+
const normalized = value.replace(/\s+/g, "").trim();
|
|
33
|
+
if (normalized.length <= maxLength)
|
|
34
|
+
return normalized;
|
|
35
|
+
return `${normalized.slice(0, maxLength)}... [truncated, ${normalized.length} chars total]`;
|
|
36
|
+
}
|
|
37
|
+
function classifyCertificateSource(srcRaw) {
|
|
38
|
+
if (srcRaw === "system")
|
|
39
|
+
return { sourceKind: "system" };
|
|
40
|
+
if (srcRaw === "user")
|
|
41
|
+
return { sourceKind: "user" };
|
|
42
|
+
if (srcRaw?.startsWith("@raw/"))
|
|
43
|
+
return { sourceKind: "raw-resource", rawResourceName: srcRaw.slice("@raw/".length) };
|
|
44
|
+
return { sourceKind: "unknown" };
|
|
45
|
+
}
|
|
46
|
+
function parseTrustAnchors(element) {
|
|
47
|
+
const certificates = findChildren(element, "certificates").map((certEl) => {
|
|
48
|
+
const srcRaw = getUnprefixedAttribute(certEl, "src");
|
|
49
|
+
const { sourceKind, rawResourceName } = classifyCertificateSource(srcRaw);
|
|
50
|
+
const overridePinsResult = parseBooleanAttribute(certEl, "overridePins");
|
|
51
|
+
return {
|
|
52
|
+
srcRaw,
|
|
53
|
+
sourceKind,
|
|
54
|
+
rawResourceName,
|
|
55
|
+
overridePins: overridePinsResult.value,
|
|
56
|
+
overridePinsRaw: overridePinsResult.raw,
|
|
57
|
+
location: certEl.location,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
return { certificates, location: element.location };
|
|
61
|
+
}
|
|
62
|
+
function parsePinEntry(pinEl) {
|
|
63
|
+
const digestRaw = getUnprefixedAttribute(pinEl, "digest");
|
|
64
|
+
const digestSupported = digestRaw !== undefined && SUPPORTED_PIN_DIGESTS.has(digestRaw);
|
|
65
|
+
const rawText = pinEl.textContent.trim();
|
|
66
|
+
const malformed = rawText.length === 0 || digestRaw === undefined;
|
|
67
|
+
const malformedReason = rawText.length === 0 ? "Pin element has no text content" : digestRaw === undefined ? "Pin element is missing a digest attribute" : undefined;
|
|
68
|
+
return {
|
|
69
|
+
digestRaw,
|
|
70
|
+
digestSupported,
|
|
71
|
+
valuePreview: boundedPreview(rawText),
|
|
72
|
+
malformed,
|
|
73
|
+
malformedReason,
|
|
74
|
+
location: pinEl.location,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function parsePinSet(element) {
|
|
78
|
+
const expirationRaw = getUnprefixedAttribute(element, "expiration");
|
|
79
|
+
// A syntactically valid expiration is YYYY-MM-DD; anything else is
|
|
80
|
+
// preserved verbatim and flagged malformed rather than guessed at.
|
|
81
|
+
const expirationMalformed = expirationRaw !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(expirationRaw);
|
|
82
|
+
const pins = findChildren(element, "pin").map(parsePinEntry);
|
|
83
|
+
return { expirationRaw, expirationMalformed, pins, location: element.location };
|
|
84
|
+
}
|
|
85
|
+
function normalizeDomainText(raw) {
|
|
86
|
+
let normalized = raw.trim().toLowerCase();
|
|
87
|
+
if (normalized.endsWith(".") && normalized.length > 1) {
|
|
88
|
+
normalized = normalized.slice(0, -1);
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
function parseDomainEntry(domainEl) {
|
|
93
|
+
const includeSubdomainsResult = parseBooleanAttribute(domainEl, "includeSubdomains");
|
|
94
|
+
const textRaw = domainEl.textContent;
|
|
95
|
+
const normalized = normalizeDomainText(textRaw);
|
|
96
|
+
return {
|
|
97
|
+
textRaw,
|
|
98
|
+
normalized,
|
|
99
|
+
includeSubdomains: includeSubdomainsResult.value,
|
|
100
|
+
includeSubdomainsRaw: includeSubdomainsResult.raw,
|
|
101
|
+
malformed: normalized.length === 0,
|
|
102
|
+
location: domainEl.location,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const DOMAIN_CONFIG_KNOWN_CHILDREN = new Set(["domain", "trust-anchors", "pin-set", "domain-config"]);
|
|
106
|
+
function parseDomainConfig(element, depth, warnings) {
|
|
107
|
+
const cleartextResult = parseBooleanAttribute(element, "cleartextTrafficPermitted");
|
|
108
|
+
const domains = findChildren(element, "domain").map(parseDomainEntry);
|
|
109
|
+
const trustAnchorsEl = findChildren(element, "trust-anchors")[0];
|
|
110
|
+
const pinSetEl = findChildren(element, "pin-set")[0];
|
|
111
|
+
const unsupportedChildren = element.children.filter((child) => !DOMAIN_CONFIG_KNOWN_CHILDREN.has(child.localName)).map((child) => `<${child.tagName}> at line ${child.location.line}`);
|
|
112
|
+
const nestedElements = findChildren(element, "domain-config");
|
|
113
|
+
let nestingTruncated = false;
|
|
114
|
+
let nested = [];
|
|
115
|
+
if (nestedElements.length > 0) {
|
|
116
|
+
if (depth >= MAX_DOMAIN_CONFIG_DEPTH) {
|
|
117
|
+
nestingTruncated = true;
|
|
118
|
+
warnings.push(`<domain-config> nesting exceeds the bounded depth of ${MAX_DOMAIN_CONFIG_DEPTH} at line ${element.location.line}; deeper nodes were not parsed.`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
nested = nestedElements.map((child) => parseDomainConfig(child, depth + 1, warnings));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
cleartextTrafficPermitted: cleartextResult.value,
|
|
126
|
+
cleartextTrafficPermittedRaw: cleartextResult.raw,
|
|
127
|
+
domains,
|
|
128
|
+
trustAnchors: trustAnchorsEl ? parseTrustAnchors(trustAnchorsEl) : undefined,
|
|
129
|
+
pinSet: pinSetEl ? parsePinSet(pinSetEl) : undefined,
|
|
130
|
+
nested,
|
|
131
|
+
location: element.location,
|
|
132
|
+
unsupportedChildren,
|
|
133
|
+
nestingTruncated,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function parseBaseConfig(element) {
|
|
137
|
+
const cleartextResult = parseBooleanAttribute(element, "cleartextTrafficPermitted");
|
|
138
|
+
const trustAnchorsEl = findChildren(element, "trust-anchors")[0];
|
|
139
|
+
const pinSetEl = findChildren(element, "pin-set")[0];
|
|
140
|
+
return {
|
|
141
|
+
cleartextTrafficPermitted: cleartextResult.value,
|
|
142
|
+
cleartextTrafficPermittedRaw: cleartextResult.raw,
|
|
143
|
+
trustAnchors: trustAnchorsEl ? parseTrustAnchors(trustAnchorsEl) : undefined,
|
|
144
|
+
pinSet: pinSetEl ? parsePinSet(pinSetEl) : undefined,
|
|
145
|
+
location: element.location,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function parseDebugOverrides(element) {
|
|
149
|
+
const trustAnchorsEl = findChildren(element, "trust-anchors")[0];
|
|
150
|
+
return { trustAnchors: trustAnchorsEl ? parseTrustAnchors(trustAnchorsEl) : undefined, location: element.location };
|
|
151
|
+
}
|
|
152
|
+
// Parses Network Security Config XML text. Never throws for ordinary
|
|
153
|
+
// malformed external input — always returns a structured result.
|
|
154
|
+
export function parseNetworkSecurityConfig(xmlText) {
|
|
155
|
+
const parsed = parseXmlDocument(xmlText);
|
|
156
|
+
if (!parsed.ok) {
|
|
157
|
+
return { state: "malformed-xml", reason: parsed.error.message, location: parsed.error.location };
|
|
158
|
+
}
|
|
159
|
+
const root = parsed.root;
|
|
160
|
+
if (root.localName !== "network-security-config") {
|
|
161
|
+
return { state: "unsupported-root", rootTagName: root.tagName };
|
|
162
|
+
}
|
|
163
|
+
const warnings = parsed.warnings.map((w) => `${w.message} (line ${w.location.line})`);
|
|
164
|
+
const baseConfigEl = findChildren(root, "base-config")[0];
|
|
165
|
+
if (findChildren(root, "base-config").length > 1) {
|
|
166
|
+
warnings.push(`Multiple <base-config> elements found; only the first was used.`);
|
|
167
|
+
}
|
|
168
|
+
const debugOverridesEl = findChildren(root, "debug-overrides")[0];
|
|
169
|
+
if (findChildren(root, "debug-overrides").length > 1) {
|
|
170
|
+
warnings.push(`Multiple <debug-overrides> elements found; only the first was used.`);
|
|
171
|
+
}
|
|
172
|
+
const domainConfigEls = findChildren(root, "domain-config");
|
|
173
|
+
const knownRootChildren = new Set(["base-config", "domain-config", "debug-overrides"]);
|
|
174
|
+
for (const child of root.children) {
|
|
175
|
+
if (!knownRootChildren.has(child.localName)) {
|
|
176
|
+
warnings.push(`Unsupported <${child.tagName}> element at line ${child.location.line} was not parsed.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
state: "parsed",
|
|
181
|
+
baseConfig: baseConfigEl ? parseBaseConfig(baseConfigEl) : undefined,
|
|
182
|
+
domainConfigs: domainConfigEls.map((el) => parseDomainConfig(el, 0, warnings)),
|
|
183
|
+
debugOverrides: debugOverridesEl ? parseDebugOverrides(debugOverridesEl) : undefined,
|
|
184
|
+
warnings,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAX_DOMAIN_CONFIG_DEPTH = 10;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 1 — deterministic ordering for candidate evidence produced by
|
|
3
|
+
// later v0.4.1 detectors. Mirrors the existing localeCompare-by-id pattern
|
|
4
|
+
// already used for finding/module ordering (e.g.
|
|
5
|
+
// src/mobile/android/validate/validateAndroidTarget.ts's
|
|
6
|
+
// `.sort((a, b) => a.id.localeCompare(b.id))`, detect/detectAndroidProject.ts)
|
|
7
|
+
// rather than introducing a different comparator convention.
|
|
8
|
+
//
|
|
9
|
+
// Ordering key precedence: category, then rule id, then module path, then
|
|
10
|
+
// source path, then line, then column, then fingerprint (final tiebreaker —
|
|
11
|
+
// guarantees a total order even for two otherwise-identical locations).
|
|
12
|
+
// Must never depend on filesystem enumeration order or object insertion
|
|
13
|
+
// order.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
function compareOptionalNumber(a, b) {
|
|
16
|
+
if (a === b)
|
|
17
|
+
return 0;
|
|
18
|
+
if (a === undefined)
|
|
19
|
+
return -1;
|
|
20
|
+
if (b === undefined)
|
|
21
|
+
return 1;
|
|
22
|
+
return a - b;
|
|
23
|
+
}
|
|
24
|
+
function compareOptionalString(a, b) {
|
|
25
|
+
if (a === b)
|
|
26
|
+
return 0;
|
|
27
|
+
if (a === undefined)
|
|
28
|
+
return -1;
|
|
29
|
+
if (b === undefined)
|
|
30
|
+
return 1;
|
|
31
|
+
return a.localeCompare(b);
|
|
32
|
+
}
|
|
33
|
+
export function compareCandidateEvidence(a, b) {
|
|
34
|
+
return (a.category.localeCompare(b.category) ||
|
|
35
|
+
a.ruleId.localeCompare(b.ruleId) ||
|
|
36
|
+
compareOptionalString(a.modulePath, b.modulePath) ||
|
|
37
|
+
a.location.path.localeCompare(b.location.path) ||
|
|
38
|
+
compareOptionalNumber(a.location.line, b.location.line) ||
|
|
39
|
+
compareOptionalNumber(a.location.column, b.location.column) ||
|
|
40
|
+
a.fingerprint.localeCompare(b.fingerprint));
|
|
41
|
+
}
|
|
42
|
+
export function sortCandidateEvidence(evidence) {
|
|
43
|
+
return [...evidence].sort(compareCandidateEvidence);
|
|
44
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { stripUnsafeControlChars } from "../../../securityValidation/attackScenarios/exploitEvidence.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.1 Batch 1 — redaction and fingerprint primitives for later Android
|
|
5
|
+
// secret/signing/configuration candidate evidence.
|
|
6
|
+
//
|
|
7
|
+
// Reuses stripUnsafeControlChars from the existing exploit-evidence redaction
|
|
8
|
+
// owner (src/securityValidation/attackScenarios/exploitEvidence.ts) rather
|
|
9
|
+
// than re-implementing control-character stripping. That module's
|
|
10
|
+
// redactPreview() fully masks any secret-*looking* substring behind a
|
|
11
|
+
// "[REDACTED:Nchars]" marker with no prefix/suffix, which is the right
|
|
12
|
+
// behavior for exploit-scenario evidence (never show any of a real exploit
|
|
13
|
+
// payload). Android candidate evidence has a different, narrower need: a
|
|
14
|
+
// human reviewing a static-candidate finding benefits from a short prefix/
|
|
15
|
+
// suffix so they can recognize *which* secret in their own source it is
|
|
16
|
+
// without the full value ever being reconstructable — so this module adds
|
|
17
|
+
// that prefix/suffix-preserving preview and a non-reversible fingerprint on
|
|
18
|
+
// top of the shared control-character stripping, instead of creating a
|
|
19
|
+
// second, competing full-redaction implementation.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const DEFAULT_MAX_PREVIEW_LENGTH = 64;
|
|
22
|
+
const DEFAULT_EDGE_LENGTH = 3;
|
|
23
|
+
const MASK = "***";
|
|
24
|
+
// Distinguishes "no input was available to redact" (e.g. a value could not
|
|
25
|
+
// be read) from "the input was an empty string literal" — callers must not
|
|
26
|
+
// conflate the two.
|
|
27
|
+
export function redactedPreviewForCandidate(input, options = {}) {
|
|
28
|
+
if (input === undefined)
|
|
29
|
+
return "[unavailable]";
|
|
30
|
+
if (input.length === 0)
|
|
31
|
+
return "[empty]";
|
|
32
|
+
const maxLength = options.maxLength ?? DEFAULT_MAX_PREVIEW_LENGTH;
|
|
33
|
+
const edgeLength = options.edgeLength ?? DEFAULT_EDGE_LENGTH;
|
|
34
|
+
const normalized = stripUnsafeControlChars(input).replace(/\r\n|\r|\n/g, "\\n");
|
|
35
|
+
const bounded = normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
|
36
|
+
// Too short to safely show both a prefix and a suffix without
|
|
37
|
+
// reconstructing most or all of the value — mask it entirely instead.
|
|
38
|
+
if (bounded.length <= edgeLength * 2) {
|
|
39
|
+
return MASK;
|
|
40
|
+
}
|
|
41
|
+
const prefix = bounded.slice(0, edgeLength);
|
|
42
|
+
const suffix = bounded.slice(-edgeLength);
|
|
43
|
+
return `${prefix}${MASK}${suffix}`;
|
|
44
|
+
}
|
|
45
|
+
// Stable, non-reversible fingerprint for deduplication. Uses Node's built-in
|
|
46
|
+
// sha256 (no new dependency). Distinct from redactedPreviewForCandidate: the
|
|
47
|
+
// fingerprint is stored for dedup, the preview is stored for human review —
|
|
48
|
+
// neither one can be used to recover the raw value.
|
|
49
|
+
export function fingerprintCandidateValue(input) {
|
|
50
|
+
if (input === undefined)
|
|
51
|
+
return "unavailable";
|
|
52
|
+
const hash = createHash("sha256").update(input, "utf8").digest("hex");
|
|
53
|
+
return `sha256:${hash}`;
|
|
54
|
+
}
|
package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { makeAndroidFinding } from "../../audit/androidFinding.js";
|
|
3
|
+
import { buildAndroidSourceLocation } from "../sourceLocation.js";
|
|
4
|
+
import { makeCandidateEvidence } from "../candidateEvidence.js";
|
|
5
|
+
import { extractManifestReleaseEvidence } from "./manifestEvidence.js";
|
|
6
|
+
const RELEASE_BUILD_TYPE_NAME = "release";
|
|
7
|
+
function loc(targetRoot, absolutePath, position) {
|
|
8
|
+
return buildAndroidSourceLocation(targetRoot, absolutePath, position);
|
|
9
|
+
}
|
|
10
|
+
// Exact-name match only (agents.txt Batch 3 section 9.11) — no guessing by
|
|
11
|
+
// substring, no flavor/variant evaluation. All matches are preserved (a
|
|
12
|
+
// module could in principle declare more than one via extraction quirks);
|
|
13
|
+
// ambiguity is never resolved arbitrarily.
|
|
14
|
+
function findReleaseLikeBuildTypes(gradleModule) {
|
|
15
|
+
return (gradleModule?.buildTypeDetails ?? []).filter((buildType) => buildType.name === RELEASE_BUILD_TYPE_NAME);
|
|
16
|
+
}
|
|
17
|
+
export function analyzeApplicationReleaseConfiguration(targetRoot, entry, gradleModules) {
|
|
18
|
+
const manifestEvidence = extractManifestReleaseEvidence(entry);
|
|
19
|
+
const candidates = [];
|
|
20
|
+
const findings = [];
|
|
21
|
+
const evidenceText = [];
|
|
22
|
+
const modulePath = entry.modulePath;
|
|
23
|
+
const manifestAbsolutePath = path.join(targetRoot, entry.manifestPath);
|
|
24
|
+
const gradleModule = gradleModules.find((m) => m.path === modulePath);
|
|
25
|
+
// Only suppressed (downgraded to candidate) when a module is confidently
|
|
26
|
+
// library-only — an ambiguous/unknown module type is still treated as an
|
|
27
|
+
// application for conservative reporting purposes.
|
|
28
|
+
const isLibraryOnlyModule = gradleModule?.isLibrary === true && gradleModule?.isApplication !== true;
|
|
29
|
+
// --- manifest debuggable ---
|
|
30
|
+
if (manifestEvidence.debuggable.state === "explicit-true") {
|
|
31
|
+
if (isLibraryOnlyModule) {
|
|
32
|
+
candidates.push(makeCandidateEvidence({
|
|
33
|
+
ruleId: "android-release-debuggable",
|
|
34
|
+
category: "android-release-configuration",
|
|
35
|
+
confidence: "low",
|
|
36
|
+
modulePath,
|
|
37
|
+
location: loc(targetRoot, manifestAbsolutePath, manifestEvidence.applicationLocation),
|
|
38
|
+
summary: 'android:debuggable="true" declared in a library module manifest (not an installable application)',
|
|
39
|
+
rawValue: manifestEvidence.debuggable.raw,
|
|
40
|
+
resolutionState: "not-applicable",
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
findings.push(makeAndroidFinding({
|
|
45
|
+
ruleId: "android-release-debuggable",
|
|
46
|
+
title: 'Manifest explicitly enables debuggable (android:debuggable="true")',
|
|
47
|
+
severity: "major",
|
|
48
|
+
confidence: "high",
|
|
49
|
+
description: 'The manifest\'s <application> element sets android:debuggable="true". This is high-confidence static evidence of a risky configuration if shipped in a release build; it does not prove the published/installed APK is actually debuggable — Gradle build-type configuration and manifest merging are not evaluated here.',
|
|
50
|
+
manifestPath: entry.manifestPath,
|
|
51
|
+
location: manifestEvidence.applicationLocation,
|
|
52
|
+
evidenceDetails: ["source=manifest-attribute"],
|
|
53
|
+
recommendation: 'Set android:debuggable="false" (or omit it) for release builds; confirm no debug manifest overlay reaches production.',
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (manifestEvidence.debuggable.state === "malformed") {
|
|
58
|
+
candidates.push(makeCandidateEvidence({
|
|
59
|
+
ruleId: "android-release-debuggable",
|
|
60
|
+
category: "android-release-configuration",
|
|
61
|
+
confidence: "low",
|
|
62
|
+
modulePath,
|
|
63
|
+
location: loc(targetRoot, manifestAbsolutePath, manifestEvidence.applicationLocation),
|
|
64
|
+
summary: "android:debuggable has an unresolved or malformed value",
|
|
65
|
+
rawValue: manifestEvidence.debuggable.raw,
|
|
66
|
+
resolutionState: "malformed",
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
else if (manifestEvidence.debuggable.state === "explicit-false") {
|
|
70
|
+
evidenceText.push(`${entry.manifestPath}: manifest debuggable is explicitly false (safe)`);
|
|
71
|
+
}
|
|
72
|
+
// --- manifest testOnly ---
|
|
73
|
+
if (manifestEvidence.testOnly.state === "explicit-true") {
|
|
74
|
+
if (isLibraryOnlyModule) {
|
|
75
|
+
candidates.push(makeCandidateEvidence({
|
|
76
|
+
ruleId: "android-release-test-only",
|
|
77
|
+
category: "android-release-configuration",
|
|
78
|
+
confidence: "low",
|
|
79
|
+
modulePath,
|
|
80
|
+
location: loc(targetRoot, manifestAbsolutePath, manifestEvidence.applicationLocation),
|
|
81
|
+
summary: 'android:testOnly="true" declared in a library module manifest',
|
|
82
|
+
rawValue: manifestEvidence.testOnly.raw,
|
|
83
|
+
resolutionState: "not-applicable",
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
findings.push(makeAndroidFinding({
|
|
88
|
+
ruleId: "android-release-test-only",
|
|
89
|
+
title: 'Manifest explicitly marks the application test-only (android:testOnly="true")',
|
|
90
|
+
severity: "major",
|
|
91
|
+
confidence: "high",
|
|
92
|
+
description: 'The manifest\'s <application> element sets android:testOnly="true", which the platform install mechanism (and Play Store) treats as release-blocking. This does not prove the package was ever installed or published.',
|
|
93
|
+
manifestPath: entry.manifestPath,
|
|
94
|
+
location: manifestEvidence.applicationLocation,
|
|
95
|
+
evidenceDetails: ["source=manifest-attribute"],
|
|
96
|
+
recommendation: 'Remove android:testOnly="true" before release; confirm no test/debug manifest overlay reaches production.',
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (manifestEvidence.testOnly.state === "malformed") {
|
|
101
|
+
candidates.push(makeCandidateEvidence({
|
|
102
|
+
ruleId: "android-release-test-only",
|
|
103
|
+
category: "android-release-configuration",
|
|
104
|
+
confidence: "low",
|
|
105
|
+
modulePath,
|
|
106
|
+
location: loc(targetRoot, manifestAbsolutePath, manifestEvidence.applicationLocation),
|
|
107
|
+
summary: "android:testOnly has an unresolved or malformed value",
|
|
108
|
+
rawValue: manifestEvidence.testOnly.raw,
|
|
109
|
+
resolutionState: "malformed",
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
// --- Gradle release build-type ---
|
|
113
|
+
const releaseBuildTypes = findReleaseLikeBuildTypes(gradleModule);
|
|
114
|
+
const buildFileRelativePath = gradleModule?.buildFilePath;
|
|
115
|
+
const buildFileAbsolutePath = buildFileRelativePath !== undefined ? path.join(targetRoot, buildFileRelativePath) : undefined;
|
|
116
|
+
const fallbackLocation = buildFileAbsolutePath !== undefined ? loc(targetRoot, buildFileAbsolutePath) : loc(targetRoot, manifestAbsolutePath);
|
|
117
|
+
if (releaseBuildTypes.length === 0) {
|
|
118
|
+
candidates.push(makeCandidateEvidence({
|
|
119
|
+
ruleId: "android-release-build-type-configuration",
|
|
120
|
+
category: "android-release-configuration",
|
|
121
|
+
confidence: "low",
|
|
122
|
+
modulePath,
|
|
123
|
+
location: fallbackLocation,
|
|
124
|
+
summary: `No "release" build type was statically found for module ${modulePath ?? "(unknown)"}`,
|
|
125
|
+
rawValue: undefined,
|
|
126
|
+
resolutionState: "missing",
|
|
127
|
+
staticAnalysisLimitations: [
|
|
128
|
+
"A missing release build type is not automatically a vulnerability; Gradle default build types, convention plugins, and variant configuration are not evaluated.",
|
|
129
|
+
],
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
let gradleReleaseDebuggableTrue = false;
|
|
133
|
+
let gradleReleaseDebuggableFalse = false;
|
|
134
|
+
for (const buildType of releaseBuildTypes) {
|
|
135
|
+
if (buildType.debuggableState === "literal-true") {
|
|
136
|
+
gradleReleaseDebuggableTrue = true;
|
|
137
|
+
if (!isLibraryOnlyModule) {
|
|
138
|
+
findings.push(makeAndroidFinding({
|
|
139
|
+
ruleId: "android-release-debuggable",
|
|
140
|
+
title: `Release build type "${buildType.name}" sets debuggable=true`,
|
|
141
|
+
severity: "major",
|
|
142
|
+
confidence: "high",
|
|
143
|
+
description: `The Gradle release build type "${buildType.name}" sets a literal debuggable=true. This is high-confidence static evidence; it does not prove a built APK is actually debuggable — build-type/flavor/variant selection and Gradle conventions are not evaluated.`,
|
|
144
|
+
manifestPath: buildFileRelativePath ?? entry.manifestPath,
|
|
145
|
+
identity: buildType.name,
|
|
146
|
+
evidenceDetails: [`buildType=${buildType.name}`, "source=gradle-literal"],
|
|
147
|
+
recommendation: "Set debuggable=false (or remove the override) for the release build type.",
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
candidates.push(makeCandidateEvidence({
|
|
152
|
+
ruleId: "android-release-debuggable",
|
|
153
|
+
category: "android-release-configuration",
|
|
154
|
+
confidence: "low",
|
|
155
|
+
modulePath,
|
|
156
|
+
location: fallbackLocation,
|
|
157
|
+
summary: `Library module's "${buildType.name}" build type sets debuggable=true (not an installable application)`,
|
|
158
|
+
rawValue: buildType.debuggableRaw,
|
|
159
|
+
resolutionState: "not-applicable",
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (buildType.debuggableState === "literal-false") {
|
|
164
|
+
gradleReleaseDebuggableFalse = true;
|
|
165
|
+
evidenceText.push(`${modulePath ?? "(unknown module)"}: release build type "${buildType.name}" debuggable is explicitly false (safe)`);
|
|
166
|
+
}
|
|
167
|
+
else if (buildType.debuggableState === "dynamic") {
|
|
168
|
+
candidates.push(makeCandidateEvidence({
|
|
169
|
+
ruleId: "android-release-debuggable",
|
|
170
|
+
category: "android-release-configuration",
|
|
171
|
+
confidence: "medium",
|
|
172
|
+
modulePath,
|
|
173
|
+
location: fallbackLocation,
|
|
174
|
+
summary: `Release build type "${buildType.name}" debuggable value is a dynamic/unresolved expression`,
|
|
175
|
+
rawValue: buildType.debuggableRaw,
|
|
176
|
+
resolutionState: "unresolved",
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
// Minification/resource-shrinking/signingConfig — release-hardening
|
|
180
|
+
// metadata only, never a finding (agents.txt Batch 3 sections 9.13/9.16).
|
|
181
|
+
evidenceText.push(`${modulePath ?? "(unknown module)"}: release build type "${buildType.name}" minifyEnabled=${buildType.minifyEnabledState}, shrinkResources=${buildType.shrinkResourcesState}`);
|
|
182
|
+
if (buildType.signingConfigRef !== undefined) {
|
|
183
|
+
evidenceText.push(`${modulePath ?? "(unknown module)"}: release build type "${buildType.name}" signingConfig=${buildType.signingConfigRef}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// --- manifest/Gradle correlation (never invents a build-time winner) ---
|
|
187
|
+
if (manifestEvidence.debuggable.state === "explicit-true" && gradleReleaseDebuggableTrue) {
|
|
188
|
+
evidenceText.push(`${entry.manifestPath}: manifest debuggable=true aligns with release build type debuggable=true (aligned risky evidence)`);
|
|
189
|
+
}
|
|
190
|
+
else if (manifestEvidence.debuggable.state === "explicit-true" && gradleReleaseDebuggableFalse) {
|
|
191
|
+
candidates.push(makeCandidateEvidence({
|
|
192
|
+
ruleId: "android-release-debuggable",
|
|
193
|
+
category: "android-release-configuration",
|
|
194
|
+
confidence: "medium",
|
|
195
|
+
modulePath,
|
|
196
|
+
location: loc(targetRoot, manifestAbsolutePath, manifestEvidence.applicationLocation),
|
|
197
|
+
summary: "Manifest debuggable=true conflicts with the release build type's literal debuggable=false",
|
|
198
|
+
rawValue: manifestEvidence.debuggable.raw,
|
|
199
|
+
resolutionState: "resolved",
|
|
200
|
+
staticAnalysisLimitations: [
|
|
201
|
+
"Precedence between the manifest attribute and Gradle build-type configuration was not evaluated; this is conflicting static evidence, not a confirmed build-time value.",
|
|
202
|
+
],
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
else if (manifestEvidence.debuggable.state === "explicit-false" && gradleReleaseDebuggableTrue) {
|
|
206
|
+
candidates.push(makeCandidateEvidence({
|
|
207
|
+
ruleId: "android-release-debuggable",
|
|
208
|
+
category: "android-release-configuration",
|
|
209
|
+
confidence: "medium",
|
|
210
|
+
modulePath,
|
|
211
|
+
location: loc(targetRoot, manifestAbsolutePath, manifestEvidence.applicationLocation),
|
|
212
|
+
summary: "Manifest debuggable=false conflicts with the release build type's literal debuggable=true",
|
|
213
|
+
rawValue: manifestEvidence.debuggable.raw,
|
|
214
|
+
resolutionState: "resolved",
|
|
215
|
+
staticAnalysisLimitations: [
|
|
216
|
+
"Precedence between the manifest attribute and Gradle build-type configuration was not evaluated; this is conflicting static evidence, not a confirmed build-time value.",
|
|
217
|
+
],
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
return { manifestEvidence, candidates, findings, evidenceText };
|
|
221
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { buildAndroidManifestCheckResult } from "../../audit/checkResultBuilder.js";
|
|
2
|
+
import { sortCandidateEvidence } from "../ordering.js";
|
|
3
|
+
import { analyzeApplicationReleaseConfiguration } from "./analyzeReleaseConfiguration.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// v0.4.1 Batch 3 — standalone Android debuggable/testOnly/release build-type
|
|
6
|
+
// check result. Mirrors Batch 2/Batch 3-backup's checkResult.ts convention.
|
|
7
|
+
// Standalone: not called from validateAndroidTarget / any active
|
|
8
|
+
// orchestration, not rendered in reports, no CLI effect. Batch 8 integrates.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export const ANDROID_RELEASE_CONFIGURATION_AUDIT_CHECK_ID = "android-release-configuration-audit";
|
|
11
|
+
export function auditAndroidReleaseConfiguration(targetRoot, detection, manifests, gradleModules) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
const candidateEvidence = [];
|
|
14
|
+
const evidence = [];
|
|
15
|
+
const warnings = [];
|
|
16
|
+
for (const entry of manifests) {
|
|
17
|
+
const result = analyzeApplicationReleaseConfiguration(targetRoot, entry, gradleModules);
|
|
18
|
+
findings.push(...result.findings);
|
|
19
|
+
candidateEvidence.push(...result.candidates);
|
|
20
|
+
evidence.push(`${entry.manifestPath}: debuggable=${result.manifestEvidence.debuggable.state}, testOnly=${result.manifestEvidence.testOnly.state}`, ...result.evidenceText);
|
|
21
|
+
}
|
|
22
|
+
const base = buildAndroidManifestCheckResult({
|
|
23
|
+
id: ANDROID_RELEASE_CONFIGURATION_AUDIT_CHECK_ID,
|
|
24
|
+
category: "android-release-configuration",
|
|
25
|
+
title: "Android debuggable, testOnly, and release build-type configuration audit",
|
|
26
|
+
detection,
|
|
27
|
+
manifests,
|
|
28
|
+
findings,
|
|
29
|
+
evidence,
|
|
30
|
+
warnings: [
|
|
31
|
+
...warnings,
|
|
32
|
+
"Static analysis only: does not evaluate Gradle variant/flavor selection, manifest merging, or whether a published APK is actually debuggable.",
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
...base,
|
|
37
|
+
candidateEvidence: sortCandidateEvidence(candidateEvidence),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 3 — public exports for the standalone Android debuggable/
|
|
3
|
+
// testOnly/release build-type audit.
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export { extractManifestReleaseEvidence } from "./manifestEvidence.js";
|
|
6
|
+
export { analyzeApplicationReleaseConfiguration } from "./analyzeReleaseConfiguration.js";
|
|
7
|
+
export { auditAndroidReleaseConfiguration, ANDROID_RELEASE_CONFIGURATION_AUDIT_CHECK_ID } from "./checkResult.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function classifyBooleanAttribute(value, raw) {
|
|
2
|
+
if (value === true)
|
|
3
|
+
return { state: "explicit-true", raw: raw ?? "true" };
|
|
4
|
+
if (value === false)
|
|
5
|
+
return { state: "explicit-false", raw: raw ?? "false" };
|
|
6
|
+
if (raw !== undefined)
|
|
7
|
+
return { state: "malformed", raw };
|
|
8
|
+
return { state: "missing" };
|
|
9
|
+
}
|
|
10
|
+
export function extractManifestReleaseEvidence(entry) {
|
|
11
|
+
const app = entry.manifest.application;
|
|
12
|
+
return {
|
|
13
|
+
manifestPath: entry.manifestPath,
|
|
14
|
+
modulePath: entry.modulePath,
|
|
15
|
+
applicationLocation: app.location,
|
|
16
|
+
debuggable: classifyBooleanAttribute(app.debuggable, app.debuggableRaw),
|
|
17
|
+
testOnly: classifyBooleanAttribute(app.testOnly, app.testOnlyRaw),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 1 — Android resource-reference parsing (`@xml/...` and
|
|
3
|
+
// similar). This is genuinely new: no resource-reference resolution exists
|
|
4
|
+
// anywhere in the codebase yet (the manifest parser stores refs like
|
|
5
|
+
// `application.networkSecurityConfigRef` verbatim, by design — see
|
|
6
|
+
// src/mobile/android/manifest/types.ts's comment). This module only parses
|
|
7
|
+
// the reference string into a structured shape; resourceResolution.ts
|
|
8
|
+
// resolves it against the filesystem.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export const SUPPORTED_ANDROID_RESOURCE_TYPES = ["xml"];
|
|
11
|
+
const RESOURCE_NAME_PATTERN = /^[a-z0-9_]+$/;
|
|
12
|
+
// Parses a raw manifest/resource attribute value such as `@xml/network_security_config`.
|
|
13
|
+
// Never throws — every input maps to a structured state.
|
|
14
|
+
export function parseAndroidResourceReference(raw) {
|
|
15
|
+
if (raw.length === 0) {
|
|
16
|
+
return { state: "empty", raw };
|
|
17
|
+
}
|
|
18
|
+
const trimmed = raw.trim();
|
|
19
|
+
if (trimmed.length === 0) {
|
|
20
|
+
return { state: "malformed", raw, reason: "Reference is whitespace-only" };
|
|
21
|
+
}
|
|
22
|
+
// A `@string/app_name`-style placeholder that itself resolves to another
|
|
23
|
+
// resource is out of scope for this parser (only concrete @xml/... value
|
|
24
|
+
// references are supported); an unresolved build-variable placeholder such
|
|
25
|
+
// as `${networkSecurityConfig}` is reported distinctly.
|
|
26
|
+
if (/^\$\{.*\}$/.test(trimmed)) {
|
|
27
|
+
return { state: "placeholder", raw };
|
|
28
|
+
}
|
|
29
|
+
if (!trimmed.startsWith("@")) {
|
|
30
|
+
return { state: "malformed", raw, reason: "Reference does not start with \"@\"" };
|
|
31
|
+
}
|
|
32
|
+
const body = trimmed.slice(1);
|
|
33
|
+
if (body.length === 0) {
|
|
34
|
+
return { state: "malformed", raw, reason: "Reference has no content after \"@\"" };
|
|
35
|
+
}
|
|
36
|
+
// Package-qualified external reference: @com.example.lib:xml/name
|
|
37
|
+
const colonIndex = body.indexOf(":");
|
|
38
|
+
let packageQualifier;
|
|
39
|
+
let rest = body;
|
|
40
|
+
if (colonIndex !== -1) {
|
|
41
|
+
packageQualifier = body.slice(0, colonIndex);
|
|
42
|
+
rest = body.slice(colonIndex + 1);
|
|
43
|
+
if (packageQualifier.length === 0) {
|
|
44
|
+
return { state: "malformed", raw, reason: "Empty package qualifier before \":\"" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const slashIndex = rest.indexOf("/");
|
|
48
|
+
if (slashIndex === -1) {
|
|
49
|
+
return { state: "malformed", raw, reason: "Reference is missing a \"/\" between resource type and name" };
|
|
50
|
+
}
|
|
51
|
+
const type = rest.slice(0, slashIndex);
|
|
52
|
+
const name = rest.slice(slashIndex + 1);
|
|
53
|
+
if (type.length === 0) {
|
|
54
|
+
return { state: "malformed", raw, reason: "Reference is missing a resource type" };
|
|
55
|
+
}
|
|
56
|
+
if (name.length === 0) {
|
|
57
|
+
return { state: "malformed", raw, reason: "Reference is missing a resource name" };
|
|
58
|
+
}
|
|
59
|
+
if (!RESOURCE_NAME_PATTERN.test(name)) {
|
|
60
|
+
return { state: "malformed", raw, reason: `Resource name "${name}" is not a valid Android resource identifier` };
|
|
61
|
+
}
|
|
62
|
+
if (packageQualifier !== undefined) {
|
|
63
|
+
return { state: "package-qualified", raw, packageQualifier, type, name };
|
|
64
|
+
}
|
|
65
|
+
if (!SUPPORTED_ANDROID_RESOURCE_TYPES.includes(type)) {
|
|
66
|
+
return { state: "unsupported-type", raw, type, name };
|
|
67
|
+
}
|
|
68
|
+
return { state: "parsed", raw, type: type, name };
|
|
69
|
+
}
|