@dailephd/my-dev-kit-lab 0.4.0 → 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.
Files changed (147) hide show
  1. package/README.md +10 -61
  2. package/dist/scripts/security/validate.js +38 -5
  3. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
  4. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
  5. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
  6. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
  7. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
  8. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
  9. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
  10. package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
  11. package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
  12. package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
  13. package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
  14. package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
  15. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
  16. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
  17. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
  18. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
  19. package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
  20. package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
  21. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
  22. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
  23. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
  24. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
  25. package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
  26. package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
  27. package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
  28. package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
  29. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
  30. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
  31. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
  32. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
  33. package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
  34. package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
  35. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
  36. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
  37. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
  38. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
  39. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
  40. package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
  41. package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
  42. package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
  43. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
  44. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
  45. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
  46. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
  47. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
  48. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
  49. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
  50. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
  51. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
  52. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
  53. package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
  54. package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
  55. package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
  56. package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
  57. package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
  58. package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
  59. package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
  60. package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
  61. package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
  62. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
  63. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
  64. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
  65. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
  66. package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
  67. package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
  68. package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
  69. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
  70. package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
  71. package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
  72. package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
  73. package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
  74. package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
  75. package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
  76. package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
  77. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
  78. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
  79. package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
  80. package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
  81. package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
  82. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
  83. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
  84. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
  85. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
  86. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
  87. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
  88. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
  89. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
  90. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
  91. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
  92. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
  93. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
  94. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
  95. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
  96. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
  97. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
  98. package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
  99. package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
  100. package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
  101. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +123 -17
  102. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +27 -4
  103. package/dist/src/mobile/android/manifest/xml/parseXml.js +25 -9
  104. package/dist/src/mobile/android/report/model.js +32 -1
  105. package/dist/src/mobile/android/report/renderAndroidReport.js +63 -0
  106. package/dist/src/mobile/android/validate/validateAndroidTarget.js +146 -3
  107. package/dist/src/mobile/android/validation/checkResult.js +7 -0
  108. package/dist/src/securityValidation/commandRunner.js +10 -1
  109. package/dist/src/securityValidation/validate/cliOptions.js +46 -0
  110. package/package.json +1 -1
  111. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
  112. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
  113. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
  114. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
  115. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
  116. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
  117. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
  118. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  119. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
  120. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
  121. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
  122. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
  123. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
  124. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
  125. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
  126. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
  127. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  128. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
  129. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
  130. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
  131. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
  132. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
  133. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
  134. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
  135. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
  136. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
  137. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
  138. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
  139. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
  140. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
  141. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
  142. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
  143. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
  144. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
  145. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
  146. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
  147. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
@@ -0,0 +1,110 @@
1
+ import { findChildren, parseXmlDocument } from "../../manifest/xml/parseXml.js";
2
+ import { BACKUP_DOMAIN_VALUES, MAX_BACKUP_RULES_PER_SCOPE, } from "./types.js";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.4.1 Batch 3 — dependency-free parser for the legacy
5
+ // <full-backup-content> and newer <data-extraction-rules> XML formats.
6
+ //
7
+ // Builds on the existing bounded XML tree parser
8
+ // (src/mobile/android/manifest/xml/parseXml.ts), mirroring Batch 2's
9
+ // parseNetworkSecurityConfig.ts layering — no second XML tokenizer. Parses
10
+ // exactly one already-resolved, target-contained file; never merges files,
11
+ // never evaluates resource overlays, never invokes Gradle/Android SDK/
12
+ // network. Malformed XML or an unsupported root never throws.
13
+ // ---------------------------------------------------------------------------
14
+ function getUnprefixedAttribute(element, localName) {
15
+ return element.attributes.find((attr) => attr.prefix === undefined && attr.localName === localName)?.value;
16
+ }
17
+ function parseBooleanAttribute(element, localName) {
18
+ const raw = getUnprefixedAttribute(element, localName);
19
+ if (raw === undefined)
20
+ return {};
21
+ if (raw === "true")
22
+ return { value: true, raw };
23
+ if (raw === "false")
24
+ return { value: false, raw };
25
+ return { raw };
26
+ }
27
+ function isBackupDomainValue(value) {
28
+ return BACKUP_DOMAIN_VALUES.includes(value);
29
+ }
30
+ function parseRuleEntry(kind, element) {
31
+ const domainRaw = getUnprefixedAttribute(element, "domain");
32
+ const path = getUnprefixedAttribute(element, "path");
33
+ const requireFlagsRaw = getUnprefixedAttribute(element, "requireFlags");
34
+ const domain = domainRaw !== undefined && isBackupDomainValue(domainRaw) ? domainRaw : undefined;
35
+ const malformed = domainRaw === undefined || path === undefined || path.length === 0;
36
+ const malformedReason = domainRaw === undefined ? "Missing domain attribute" : path === undefined || path.length === 0 ? "Missing or empty path attribute" : undefined;
37
+ return {
38
+ kind,
39
+ domainRaw,
40
+ domain,
41
+ path,
42
+ requireFlagsRaw,
43
+ location: element.location,
44
+ malformed,
45
+ malformedReason,
46
+ };
47
+ }
48
+ function parseRuleEntries(scopeElement) {
49
+ const includeEntries = findChildren(scopeElement, "include").map((el) => parseRuleEntry("include", el));
50
+ const excludeEntries = findChildren(scopeElement, "exclude").map((el) => parseRuleEntry("exclude", el));
51
+ // Preserves document order (include/exclude interleaving is semantically
52
+ // meaningful in the real format) rather than grouping by kind — includes
53
+ // are collected first only because findChildren is kind-specific; the
54
+ // caller should not assume this array reflects source order for mixed
55
+ // kinds beyond what findChildren naturally preserves per kind.
56
+ return [...includeEntries, ...excludeEntries].slice(0, MAX_BACKUP_RULES_PER_SCOPE);
57
+ }
58
+ function parseFullBackupContent(root, warnings) {
59
+ const knownChildren = new Set(["include", "exclude"]);
60
+ const unsupportedChildren = root.children.filter((child) => !knownChildren.has(child.localName)).map((child) => `<${child.tagName}> at line ${child.location.line}`);
61
+ const rules = parseRuleEntries(root);
62
+ if (rules.length >= MAX_BACKUP_RULES_PER_SCOPE) {
63
+ warnings.push(`<full-backup-content> rule count reached the bounded maximum of ${MAX_BACKUP_RULES_PER_SCOPE}; further rules were not processed.`);
64
+ }
65
+ return { rules, unsupportedChildren };
66
+ }
67
+ function parseDataExtractionRuleSet(element) {
68
+ const disableResult = parseBooleanAttribute(element, "disableIfNoEncryptionCapabilities");
69
+ return {
70
+ disableIfNoEncryptionCapabilities: disableResult.value,
71
+ disableIfNoEncryptionCapabilitiesRaw: disableResult.raw,
72
+ rules: parseRuleEntries(element),
73
+ location: element.location,
74
+ };
75
+ }
76
+ function parseDataExtractionRules(root, warnings) {
77
+ const knownChildren = new Set(["cloud-backup", "device-transfer"]);
78
+ const unsupportedChildren = root.children.filter((child) => !knownChildren.has(child.localName)).map((child) => `<${child.tagName}> at line ${child.location.line}`);
79
+ const cloudBackupEl = findChildren(root, "cloud-backup")[0];
80
+ if (findChildren(root, "cloud-backup").length > 1) {
81
+ warnings.push("Multiple <cloud-backup> elements found; only the first was used.");
82
+ }
83
+ const deviceTransferEl = findChildren(root, "device-transfer")[0];
84
+ if (findChildren(root, "device-transfer").length > 1) {
85
+ warnings.push("Multiple <device-transfer> elements found; only the first was used.");
86
+ }
87
+ return {
88
+ cloudBackup: cloudBackupEl ? parseDataExtractionRuleSet(cloudBackupEl) : undefined,
89
+ deviceTransfer: deviceTransferEl ? parseDataExtractionRuleSet(deviceTransferEl) : undefined,
90
+ unsupportedChildren,
91
+ };
92
+ }
93
+ // Parses either legacy full-backup-content or data-extraction-rules XML
94
+ // text, selecting the semantic model by the document's actual root element.
95
+ // Never throws for ordinary malformed external input.
96
+ export function parseBackupRules(xmlText) {
97
+ const parsed = parseXmlDocument(xmlText);
98
+ if (!parsed.ok) {
99
+ return { state: "malformed-xml", reason: parsed.error.message, location: parsed.error.location };
100
+ }
101
+ const root = parsed.root;
102
+ const warnings = parsed.warnings.map((w) => `${w.message} (line ${w.location.line})`);
103
+ if (root.localName === "full-backup-content") {
104
+ return { state: "parsed-full-backup-content", model: parseFullBackupContent(root, warnings), warnings };
105
+ }
106
+ if (root.localName === "data-extraction-rules") {
107
+ return { state: "parsed-data-extraction-rules", model: parseDataExtractionRules(root, warnings), warnings };
108
+ }
109
+ return { state: "unsupported-root", rootTagName: root.tagName };
110
+ }
@@ -0,0 +1,27 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 3 — bounded backup/data-extraction XML parse-tree model.
3
+ //
4
+ // Covers both the legacy <full-backup-content> format (API <=30, still
5
+ // widely used) and the newer <data-extraction-rules> format (API 31+,
6
+ // separates <cloud-backup> from <device-transfer>). Local to Android
7
+ // advanced security, mirroring Batch 2's networkSecurity/types.ts pattern —
8
+ // not pushed into the generic XML tree model. Locations are line/column
9
+ // only; file/module/source-set identity is attached by the caller once a
10
+ // file is resolved (see manifestEvidence.ts).
11
+ // ---------------------------------------------------------------------------
12
+ export const BACKUP_DOMAIN_VALUES = [
13
+ "root",
14
+ "file",
15
+ "database",
16
+ "sharedpref",
17
+ "external",
18
+ "device_root",
19
+ "device_file",
20
+ "device_database",
21
+ "device_sharedpref",
22
+ "device_external",
23
+ ];
24
+ // Defensive bound on the number of rule entries processed from one file —
25
+ // mirrors Batch 2's MAX_DOMAIN_CONFIG_DEPTH bound philosophy (never let a
26
+ // pathological fixture/target cause unbounded work).
27
+ export const MAX_BACKUP_RULES_PER_SCOPE = 500;
@@ -0,0 +1,50 @@
1
+ import { fingerprintCandidateValue, redactedPreviewForCandidate } from "./redaction.js";
2
+ // Resolution states for a static candidate's underlying evidence (e.g. did a
3
+ // referenced resource file resolve, was a signing config path found). Kept
4
+ // narrow and additive; overlaps deliberately with AndroidCheckStatus/XML
5
+ // resource-resolution states where the underlying concept is the same
6
+ // (resolved and missing/malformed/unsupported are shared vocabulary), but
7
+ // this is not a duplicate of AndroidCheckStatus — a candidate's resolution
8
+ // state describes one piece of referenced evidence, not an entire check.
9
+ export const CANDIDATE_RESOLUTION_STATES = ["resolved", "unresolved", "missing", "malformed", "unsupported", "not-applicable"];
10
+ function slug(value) {
11
+ return value
12
+ .replace(/[^a-zA-Z0-9]+/g, "-")
13
+ .replace(/^-+|-+$/g, "")
14
+ .toLowerCase();
15
+ }
16
+ // Deterministic, content-derived id (no counter, no timestamp) so equivalent
17
+ // candidate evidence always produces the same id across repeated runs.
18
+ function buildCandidateEvidenceId(ruleId, location, fingerprint) {
19
+ const locationPart = [location.path, location.line !== undefined ? `l${location.line}` : undefined, location.column !== undefined ? `c${location.column}` : undefined]
20
+ .filter((part) => Boolean(part))
21
+ .join(":");
22
+ return [ruleId, slug(locationPart), fingerprint.slice(0, 16)].join("--");
23
+ }
24
+ // Never accepts a raw secret value as a plain string parameter name to
25
+ // discourage accidental logging at call sites — `rawValue` is redacted and
26
+ // fingerprinted here, once, and the raw value is never stored on the
27
+ // returned object.
28
+ export function makeCandidateEvidence(input) {
29
+ const fingerprint = fingerprintCandidateValue(input.rawValue);
30
+ const redactedPreview = redactedPreviewForCandidate(input.rawValue);
31
+ const id = buildCandidateEvidenceId(input.ruleId, input.location, fingerprint);
32
+ return {
33
+ id,
34
+ ruleId: input.ruleId,
35
+ category: input.category,
36
+ sensitiveDataCategory: input.sensitiveDataCategory,
37
+ confidence: input.confidence,
38
+ modulePath: input.modulePath,
39
+ location: input.location,
40
+ summary: input.summary,
41
+ redactedPreview,
42
+ fingerprint,
43
+ resolutionState: input.resolutionState,
44
+ staticAnalysisLimitations: input.staticAnalysisLimitations ?? [],
45
+ relatedManifestComponentIds: input.relatedManifestComponentIds,
46
+ relatedResourceReferences: input.relatedResourceReferences,
47
+ relatedFindingIds: input.relatedFindingIds,
48
+ relatedCheckIds: input.relatedCheckIds,
49
+ };
50
+ }
@@ -0,0 +1,166 @@
1
+ import { makeAndroidFinding } from "../../audit/androidFinding.js";
2
+ import { makeCandidateEvidence } from "../candidateEvidence.js";
3
+ import { buildAndroidSourceLocation } from "../sourceLocation.js";
4
+ import { classifyDirectExpression } from "../sensitiveData/classifyDirectExpression.js";
5
+ import { classifySensitiveIdentifier, splitIdentifierWords } from "../sensitiveData/classifySensitiveIdentifier.js";
6
+ import { executableMask } from "../sensitiveData/localSourceContext.js";
7
+ import { splitTopLevelArguments } from "../sensitiveData/splitArguments.js";
8
+ import { collectClipboardEvidence } from "./collectClipboardEvidence.js";
9
+ const USER_TRIGGER_METHOD_NAMES = /^(onClick|onLongClick|onMenuItemClick|copy|copyToClipboard|handleCopy|share)$/i;
10
+ const BACKGROUND_LIKE_CLASS_SUFFIX = /(Service|Receiver|Worker)$/;
11
+ function literalOf(expression) {
12
+ if (expression === undefined)
13
+ return undefined;
14
+ const classification = classifyDirectExpression(expression);
15
+ return classification.kind === "string-literal" ? classification.literalValue : undefined;
16
+ }
17
+ function labelSensitive(labelExpression) {
18
+ if (labelExpression === undefined)
19
+ return undefined;
20
+ const literal = literalOf(labelExpression);
21
+ if (literal !== undefined) {
22
+ const words = splitIdentifierWords(literal);
23
+ for (const word of words) {
24
+ const result = classifySensitiveIdentifier(word);
25
+ if (result)
26
+ return result;
27
+ }
28
+ return undefined;
29
+ }
30
+ const identifier = classifyDirectExpression(labelExpression).kind === "identifier" ? labelExpression : undefined;
31
+ return identifier ? classifySensitiveIdentifier(identifier) : undefined;
32
+ }
33
+ function valueSensitive(valueExpression) {
34
+ if (valueExpression === undefined)
35
+ return undefined;
36
+ const classification = classifyDirectExpression(valueExpression);
37
+ if (classification.kind !== "identifier" && classification.kind !== "member-access")
38
+ return undefined;
39
+ const name = valueExpression.replace(/^.*\./, "");
40
+ return classifySensitiveIdentifier(name);
41
+ }
42
+ function nearestBefore(mask, offset, pattern) {
43
+ const window = mask.slice(Math.max(0, offset - 4000), offset);
44
+ let last = null;
45
+ let match;
46
+ const re = new RegExp(pattern.source, "g");
47
+ while ((match = re.exec(window)) !== null)
48
+ last = match;
49
+ return last?.[1];
50
+ }
51
+ export function analyzeClipboardSecurityFile(targetRoot, file) {
52
+ const candidates = [];
53
+ const findings = [];
54
+ const mask = executableMask(file.content);
55
+ const matches = collectClipboardEvidence(file.content);
56
+ const creates = matches.filter((m) => m.kind === "clip-create");
57
+ const markerOffsets = matches.filter((m) => m.kind === "sensitive-marker").map((m) => m.offset);
58
+ const clearOffsetsByScope = new Map();
59
+ for (const m of matches.filter((m) => m.kind === "clip-clear")) {
60
+ const list = clearOffsetsByScope.get(m.scopeId) ?? [];
61
+ list.push(m.offset);
62
+ clearOffsetsByScope.set(m.scopeId, list);
63
+ }
64
+ const limitations = [
65
+ "Bounded lexical source analysis; does not prove clipboard content was actually set or read, that another application accessed the clipboard, or that a UI callback executed.",
66
+ ];
67
+ const location = (m) => buildAndroidSourceLocation(targetRoot, file.absolutePath, { line: m.line });
68
+ const finding = (m, title, evidenceDetails) => findings.push(makeAndroidFinding({
69
+ ruleId: "android-sensitive-clipboard-write",
70
+ title,
71
+ severity: "major",
72
+ confidence: "high",
73
+ description: `${title} via ${m.api}. Static source evidence only; does not prove clipboard content was set at runtime or read by another application.`,
74
+ manifestPath: file.relativePath,
75
+ identity: `${m.api}:${m.line}`,
76
+ location: { line: m.line },
77
+ evidenceDetails,
78
+ recommendation: "Avoid copying credentials or recovery codes to the system clipboard; mark unavoidable sensitive clips with ClipDescription.EXTRA_IS_SENSITIVE and clear them promptly.",
79
+ }));
80
+ const candidate = (ruleId, m, summary, rawValue, confidence = "medium", resolutionState = "resolved") => candidates.push(makeCandidateEvidence({
81
+ ruleId,
82
+ category: "android-clipboard",
83
+ confidence,
84
+ modulePath: file.modulePath,
85
+ location: location(m),
86
+ summary,
87
+ rawValue,
88
+ resolutionState,
89
+ staticAnalysisLimitations: limitations,
90
+ }));
91
+ function resolveClip(argumentExpression, scopeId) {
92
+ if (argumentExpression === undefined)
93
+ return undefined;
94
+ const inline = /^ClipData\.(?:newPlainText|newHtmlText|newUri)\(([\s\S]*)\)$/.exec(argumentExpression.trim());
95
+ if (inline) {
96
+ const args = splitTopLevelArguments(inline[1]);
97
+ return { label: args[0], value: args[1], markerNearby: false };
98
+ }
99
+ const alias = classifyDirectExpression(argumentExpression).kind === "identifier" ? argumentExpression : undefined;
100
+ if (alias !== undefined) {
101
+ const source = creates.find((c) => c.assignedVariable === alias && c.scopeId === scopeId);
102
+ if (source)
103
+ return { label: source.labelExpression, value: source.valueExpression, markerNearby: false };
104
+ }
105
+ return undefined;
106
+ }
107
+ for (const m of matches) {
108
+ if (m.kind === "clip-set") {
109
+ const resolved = resolveClip(m.argumentExpression, m.scopeId);
110
+ const nearMarker = markerOffsets.some((offset) => Math.abs(offset - m.offset) < 400);
111
+ const clearedNearby = (clearOffsetsByScope.get(m.scopeId) ?? []).some((offset) => offset > m.offset);
112
+ const methodName = nearestBefore(mask, m.offset, /(?:fun|void)\s+([A-Za-z_]\w*)\s*\(/);
113
+ const userTriggered = methodName !== undefined && USER_TRIGGER_METHOD_NAMES.test(methodName);
114
+ if (!resolved) {
115
+ candidate("android-sensitive-clipboard-write", m, "Dynamic ClipData object set on the clipboard; contents not resolved", m.argumentExpression, "low", "unresolved");
116
+ continue;
117
+ }
118
+ const sensitive = labelSensitive(resolved.label) ?? valueSensitive(resolved.value);
119
+ if (!sensitive)
120
+ continue;
121
+ const valueClassification = resolved.value !== undefined ? classifyDirectExpression(resolved.value) : undefined;
122
+ if (valueClassification?.kind === "string-literal" || valueClassification?.kind === "identifier" || valueClassification?.kind === "member-access") {
123
+ const title = nearMarker
124
+ ? `Sensitive clipboard write with an EXTRA_IS_SENSITIVE marker present nearby (${sensitive.sensitiveDataCategory})`
125
+ : `Direct ${sensitive.sensitiveDataCategory} copied to the system clipboard`;
126
+ if (nearMarker || userTriggered) {
127
+ candidate("android-sensitive-clipboard-write", m, title, resolved.value, "medium");
128
+ }
129
+ else {
130
+ finding(m, title, [`label=${resolved.label ?? "(none)"}`, `marker=${nearMarker}`, `cleared=${clearedNearby}`]);
131
+ }
132
+ }
133
+ else {
134
+ candidate("android-sensitive-clipboard-write", m, `Sensitive-looking clipboard label with a dynamic payload (${sensitive.sensitiveDataCategory})`, resolved.value, "medium", "unresolved");
135
+ }
136
+ continue;
137
+ }
138
+ if (m.kind === "legacy-set-text") {
139
+ const sensitive = valueSensitive(m.valueExpression);
140
+ if (!sensitive)
141
+ continue;
142
+ const classification = m.valueExpression !== undefined ? classifyDirectExpression(m.valueExpression) : undefined;
143
+ if (classification?.kind === "string-literal" || classification?.kind === "identifier") {
144
+ finding(m, `Direct ${sensitive.sensitiveDataCategory} copied via legacy ClipboardManager.setText`, [`value=${m.valueExpression}`]);
145
+ }
146
+ else {
147
+ candidate("android-sensitive-clipboard-write", m, "Sensitive-looking legacy clipboard write with a dynamic value", m.valueExpression, "medium", "unresolved");
148
+ }
149
+ continue;
150
+ }
151
+ if (m.kind === "clip-get") {
152
+ const className = nearestBefore(mask, m.offset, /class\s+([A-Za-z_]\w*)/);
153
+ const methodName = nearestBefore(mask, m.offset, /(?:fun|void)\s+([A-Za-z_]\w*)\s*\(/);
154
+ const backgroundLike = className !== undefined && BACKGROUND_LIKE_CLASS_SUFFIX.test(className);
155
+ const userTriggered = methodName !== undefined && USER_TRIGGER_METHOD_NAMES.test(methodName);
156
+ if (backgroundLike) {
157
+ candidate("android-sensitive-clipboard-read-review", m, `Clipboard read from a background-like class (${className})`, undefined, "medium", "unresolved");
158
+ }
159
+ else if (!userTriggered) {
160
+ candidate("android-sensitive-clipboard-read-review", m, "Clipboard read outside a recognized user-triggered paste flow", undefined, "low", "unresolved");
161
+ }
162
+ continue;
163
+ }
164
+ }
165
+ return { candidates, findings };
166
+ }
@@ -0,0 +1,74 @@
1
+ import { sortCandidateEvidence } from "../ordering.js";
2
+ import { discoverSecretSourceFiles } from "../secretCandidates/discoverSecretSourceFiles.js";
3
+ import { analyzeClipboardSecurityFile } from "./analyzeClipboardSecurity.js";
4
+ // ---------------------------------------------------------------------------
5
+ // v0.4.1 Batch 6 — standalone Android clipboard-security check result.
6
+ // Follows the exact Batch 4/5 standalone pattern: not called from
7
+ // validateAndroidTarget / any active orchestration, not rendered in reports,
8
+ // no CLI effect. Batch 8 integrates.
9
+ // ---------------------------------------------------------------------------
10
+ export const ANDROID_CLIPBOARD_SECURITY_AUDIT_CHECK_ID = "android-clipboard-security-audit";
11
+ export function auditAndroidClipboardSecurity(targetRoot, detection) {
12
+ if (detection.projectKind === "non-android") {
13
+ return {
14
+ id: ANDROID_CLIPBOARD_SECURITY_AUDIT_CHECK_ID,
15
+ category: "android-clipboard",
16
+ title: "Android clipboard security audit",
17
+ status: "unsupported",
18
+ requirementLevel: "optional",
19
+ ran: false,
20
+ skipped: true,
21
+ skipInfo: {
22
+ checkId: ANDROID_CLIPBOARD_SECURITY_AUDIT_CHECK_ID,
23
+ reason: "Target was not detected as an Android project.",
24
+ requirementLevel: "optional",
25
+ missingCapability: "android-detection",
26
+ verdictImpact: "does not apply to a non-Android target",
27
+ recommendedNextAction: "Re-run against an Android Gradle project, or verify project detection.",
28
+ },
29
+ evidence: [],
30
+ findings: [],
31
+ warnings: [],
32
+ errors: [],
33
+ sourcePaths: [],
34
+ confidence: "unknown",
35
+ environmentRequirements: [],
36
+ candidateEvidence: [],
37
+ };
38
+ }
39
+ const modulePaths = detection.modules.map((m) => m.path);
40
+ const { files, skipped } = discoverSecretSourceFiles(targetRoot, modulePaths);
41
+ const sourceFiles = files.filter((file) => /\.(java|kt)$/.test(file.relativePath));
42
+ const findingsById = new Map();
43
+ const candidatesById = new Map();
44
+ for (const file of sourceFiles) {
45
+ const result = analyzeClipboardSecurityFile(targetRoot, file);
46
+ for (const f of result.findings)
47
+ findingsById.set(f.id, f);
48
+ for (const c of result.candidates)
49
+ candidatesById.set(c.id, c);
50
+ }
51
+ const findings = [...findingsById.values()].sort((a, b) => a.id.localeCompare(b.id));
52
+ const candidateEvidence = sortCandidateEvidence([...candidatesById.values()]);
53
+ const hasReportableFinding = findings.some((f) => f.severity !== "informational");
54
+ return {
55
+ id: ANDROID_CLIPBOARD_SECURITY_AUDIT_CHECK_ID,
56
+ category: "android-clipboard",
57
+ title: "Android clipboard security audit",
58
+ status: hasReportableFinding ? "failed" : "passed",
59
+ requirementLevel: "required",
60
+ ran: true,
61
+ skipped: false,
62
+ evidence: [`${sourceFiles.length} Java/Kotlin file(s) scanned`],
63
+ findings,
64
+ warnings: [
65
+ ...skipped.map((s) => `${s.relativePath}: skipped (${s.reason}${s.detail ? `: ${s.detail}` : ""})`),
66
+ "Static analysis only: does not prove clipboard content was set or read, or that another application accessed the clipboard.",
67
+ ],
68
+ errors: [],
69
+ sourcePaths: sourceFiles.map((f) => f.relativePath),
70
+ confidence: skipped.length > 0 ? "medium" : "high",
71
+ environmentRequirements: [],
72
+ candidateEvidence,
73
+ };
74
+ }
@@ -0,0 +1,82 @@
1
+ import { executableMask, lineForOffset, methodScopes, scopeAt } from "../sensitiveData/localSourceContext.js";
2
+ import { splitTopLevelArguments } from "../sensitiveData/splitArguments.js";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.4.1 Batch 6 — bounded direct evidence collection for clipboard APIs
5
+ // (ClipData creation, setPrimaryClip/getPrimaryClip/clearPrimaryClip, legacy
6
+ // android.text.ClipboardManager, and the EXTRA_IS_SENSITIVE marker). Purely
7
+ // lexical over comment/string-masked text, argument text re-sliced from
8
+ // original content via regex match indices — see
9
+ // sensitiveStorage/collectStorageEvidence.ts for the same pattern.
10
+ // ---------------------------------------------------------------------------
11
+ // Greedy (not lazy) argument capture: setPrimaryClip's single argument is
12
+ // frequently itself a nested ClipData.newPlainText(...) call, and a lazy
13
+ // quantifier would stop at that inner call's closing paren instead of the
14
+ // outer call's. Still bounded to one statement by excluding newlines/`;`.
15
+ const CLIP_CREATE = /(?:(?:val|var|ClipData)\s+([A-Za-z_]\w*)\s*=\s*)?ClipData\.(newPlainText|newHtmlText|newUri)\s*\(([^;\n]*)\)/gd;
16
+ const CLIP_SET = /([A-Za-z_][\w.]*)\.setPrimaryClip\s*\(([^;\n]*)\)/gd;
17
+ const LEGACY_SET_TEXT = /([A-Za-z_][\w.]*[Cc]lipboard[A-Za-z_]*)\.setText\s*\(([^;\n]*)\)/gd;
18
+ const CLIP_GET_CALL = /([A-Za-z_][\w.]*)\.(getPrimaryClip|hasPrimaryClip|getItemAt|coerceToText)\s*\(([^;\n]*?)\)/gd;
19
+ const CLIP_GET_PROPERTY = /([A-Za-z_][\w.]*[Cc]lipboard[A-Za-z_]*)\.(primaryClip|hasText)\b(?!\s*\()/gd;
20
+ const LEGACY_GET_TEXT = /([A-Za-z_][\w.]*[Cc]lipboard[A-Za-z_]*)\.getText\s*\(\s*\)/gd;
21
+ const CLIP_CLEAR = /([A-Za-z_][\w.]*)\.clearPrimaryClip\s*\(\s*\)/gd;
22
+ const SENSITIVE_MARKER = /\bClipDescription\.EXTRA_IS_SENSITIVE\b/g;
23
+ function realGroup(content, match, group) {
24
+ const span = match.indices?.[group];
25
+ if (!span)
26
+ return match[group];
27
+ return content.slice(span[0], span[1]);
28
+ }
29
+ function push(list, content, scopes, kind, api, offset, extra) {
30
+ list.push({ kind, api, offset, line: lineForOffset(content, offset), scopeId: scopeAt(scopes, offset)?.id, ...extra });
31
+ }
32
+ export function collectClipboardEvidence(content) {
33
+ const mask = executableMask(content);
34
+ const scopes = methodScopes(mask);
35
+ const matches = [];
36
+ for (const match of mask.matchAll(CLIP_CREATE)) {
37
+ const offset = match.index ?? 0;
38
+ const args = splitTopLevelArguments(realGroup(content, match, 3) ?? "");
39
+ push(matches, content, scopes, "clip-create", `ClipData.${match[2]}`, offset, {
40
+ assignedVariable: match[1],
41
+ labelExpression: args[0],
42
+ valueExpression: args[1],
43
+ });
44
+ }
45
+ for (const match of mask.matchAll(CLIP_SET)) {
46
+ const offset = match.index ?? 0;
47
+ const args = splitTopLevelArguments(realGroup(content, match, 2) ?? "");
48
+ push(matches, content, scopes, "clip-set", "ClipboardManager.setPrimaryClip", offset, {
49
+ receiver: match[1],
50
+ argumentExpression: args[0],
51
+ });
52
+ }
53
+ for (const match of mask.matchAll(LEGACY_SET_TEXT)) {
54
+ const offset = match.index ?? 0;
55
+ const args = splitTopLevelArguments(realGroup(content, match, 2) ?? "");
56
+ push(matches, content, scopes, "legacy-set-text", "ClipboardManager.setText", offset, {
57
+ receiver: match[1],
58
+ valueExpression: args[0],
59
+ });
60
+ }
61
+ for (const match of mask.matchAll(CLIP_GET_CALL)) {
62
+ const offset = match.index ?? 0;
63
+ push(matches, content, scopes, "clip-get", `ClipboardManager.${match[2]}`, offset, { receiver: match[1] });
64
+ }
65
+ for (const match of mask.matchAll(CLIP_GET_PROPERTY)) {
66
+ const offset = match.index ?? 0;
67
+ push(matches, content, scopes, "clip-get", `ClipboardManager.${match[2]}`, offset, { receiver: match[1] });
68
+ }
69
+ for (const match of mask.matchAll(LEGACY_GET_TEXT)) {
70
+ const offset = match.index ?? 0;
71
+ push(matches, content, scopes, "clip-get", "ClipboardManager.getText", offset, { receiver: match[1] });
72
+ }
73
+ for (const match of mask.matchAll(CLIP_CLEAR)) {
74
+ const offset = match.index ?? 0;
75
+ push(matches, content, scopes, "clip-clear", "ClipboardManager.clearPrimaryClip", offset, { receiver: match[1] });
76
+ }
77
+ for (const match of mask.matchAll(SENSITIVE_MARKER)) {
78
+ const offset = match.index ?? 0;
79
+ push(matches, content, scopes, "sensitive-marker", "ClipDescription.EXTRA_IS_SENSITIVE", offset, {});
80
+ }
81
+ return matches.sort((a, b) => a.offset - b.offset);
82
+ }
@@ -0,0 +1 @@
1
+ export { auditAndroidClipboardSecurity, ANDROID_CLIPBOARD_SECURITY_AUDIT_CHECK_ID } from "./checkResult.js";