@dailephd/my-dev-kit-lab 0.4.0 → 0.4.2

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 (158) hide show
  1. package/README.md +269 -47
  2. package/dist/scripts/audits/runAudit.js +28 -1
  3. package/dist/scripts/security/validate.js +38 -5
  4. package/dist/src/audits/core/auditConfig.js +16 -1
  5. package/dist/src/audits/core/auditRunner.js +6 -2
  6. package/dist/src/audits/report/auditReportModel.js +7 -0
  7. package/dist/src/audits/report/renderAuditJsonReport.js +1 -0
  8. package/dist/src/audits/report/renderAuditTextReport.js +37 -0
  9. package/dist/src/audits/security/androidAuditIntegration.js +145 -0
  10. package/dist/src/audits/security/mapAndroidSecurityFindingToAuditIssue.js +116 -0
  11. package/dist/src/audits/security/mapSecurityFindingToAuditIssue.js +15 -5
  12. package/dist/src/audits/security/securityAuditAdapter.js +20 -2
  13. package/dist/src/audits/security/securityAuditTypes.js +37 -0
  14. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
  15. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
  16. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
  17. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
  18. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
  19. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
  20. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
  21. package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
  22. package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
  23. package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
  24. package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
  25. package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
  26. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
  27. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
  28. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
  29. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
  30. package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
  31. package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
  32. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
  33. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
  34. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
  35. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
  36. package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
  37. package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
  38. package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
  39. package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
  40. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
  41. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
  42. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
  43. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
  44. package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
  45. package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
  46. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
  47. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
  48. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
  49. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
  50. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
  51. package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
  52. package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
  53. package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
  54. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
  55. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
  56. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
  57. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
  58. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
  59. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
  60. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
  61. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
  62. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
  63. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
  64. package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
  65. package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
  66. package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
  67. package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
  68. package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
  69. package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
  70. package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
  71. package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
  72. package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
  73. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
  74. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
  75. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
  76. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
  77. package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
  78. package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
  79. package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
  80. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
  81. package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
  82. package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
  83. package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
  84. package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
  85. package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
  86. package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
  87. package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
  88. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
  89. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
  90. package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
  91. package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
  92. package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
  93. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
  94. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
  95. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
  96. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
  97. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
  98. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
  99. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
  100. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
  101. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
  102. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
  103. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
  104. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
  105. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
  106. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
  107. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
  108. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
  109. package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
  110. package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
  111. package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
  112. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +123 -17
  113. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +27 -4
  114. package/dist/src/mobile/android/manifest/xml/parseXml.js +25 -9
  115. package/dist/src/mobile/android/report/model.js +32 -1
  116. package/dist/src/mobile/android/report/renderAndroidReport.js +63 -0
  117. package/dist/src/mobile/android/validate/validateAndroidTarget.js +146 -3
  118. package/dist/src/mobile/android/validation/checkResult.js +7 -0
  119. package/dist/src/securityValidation/commandRunner.js +10 -1
  120. package/dist/src/securityValidation/validate/cliOptions.js +46 -0
  121. package/package.json +1 -1
  122. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
  123. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
  124. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
  125. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
  126. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
  127. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
  128. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
  129. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  130. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
  131. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
  132. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
  133. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
  134. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
  135. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
  136. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
  137. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
  138. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  139. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
  140. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
  141. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
  142. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
  143. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
  144. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
  145. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
  146. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
  147. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
  148. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
  149. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
  150. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
  151. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
  152. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
  153. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
  154. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
  155. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
  156. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
  157. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
  158. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
@@ -0,0 +1,52 @@
1
+ import { MAX_DOMAIN_CONFIG_DEPTH } from "./types.js";
2
+ function walkDomainConfig(node, scopeId, depth, inheritedDomains, inheritedTrustAnchorsChain, baseCleartextTrafficPermitted, out) {
3
+ if (depth > MAX_DOMAIN_CONFIG_DEPTH)
4
+ return;
5
+ const domains = node.domains.length > 0 ? node.domains : inheritedDomains;
6
+ const domainsInherited = node.domains.length === 0 && inheritedDomains.length > 0;
7
+ const cleartextTrafficPermitted = node.cleartextTrafficPermitted ?? baseCleartextTrafficPermitted;
8
+ const cleartextTrafficSource = node.cleartextTrafficPermitted !== undefined ? "explicit" : baseCleartextTrafficPermitted !== undefined ? "inherited-from-base" : "unspecified";
9
+ const trustAnchorsChain = node.trustAnchors ? [node.trustAnchors, ...inheritedTrustAnchorsChain] : inheritedTrustAnchorsChain;
10
+ const trustAnchorsSource = node.trustAnchors ? "own" : trustAnchorsChain.length > 0 ? "inherited" : "unspecified";
11
+ out.push({
12
+ scopeId,
13
+ kind: "domain",
14
+ depth,
15
+ domains,
16
+ domainsInherited,
17
+ cleartextTrafficPermitted,
18
+ cleartextTrafficSource,
19
+ trustAnchorsChain,
20
+ trustAnchorsSource,
21
+ pinSet: node.pinSet,
22
+ location: node.location,
23
+ });
24
+ node.nested.forEach((child, index) => {
25
+ walkDomainConfig(child, `${scopeId}>domain-config[${index}]`, depth + 1, domains, trustAnchorsChain, baseCleartextTrafficPermitted, out);
26
+ });
27
+ }
28
+ // Derives deterministic effective policy from a "parsed" Network Security
29
+ // Config result. Callers must narrow to the "parsed" state first — a
30
+ // malformed/unsupported-root parse result has no policy to derive.
31
+ export function deriveEffectiveNetworkPolicy(parsed) {
32
+ const baseScope = parsed.baseConfig
33
+ ? {
34
+ scopeId: "base-config",
35
+ kind: "base",
36
+ depth: 0,
37
+ domains: [],
38
+ domainsInherited: false,
39
+ cleartextTrafficPermitted: parsed.baseConfig.cleartextTrafficPermitted,
40
+ cleartextTrafficSource: parsed.baseConfig.cleartextTrafficPermitted !== undefined ? "explicit" : "unspecified",
41
+ trustAnchorsChain: parsed.baseConfig.trustAnchors ? [parsed.baseConfig.trustAnchors] : [],
42
+ trustAnchorsSource: parsed.baseConfig.trustAnchors ? "own" : "unspecified",
43
+ pinSet: parsed.baseConfig.pinSet,
44
+ location: parsed.baseConfig.location,
45
+ }
46
+ : undefined;
47
+ const domainScopes = [];
48
+ parsed.domainConfigs.forEach((node, index) => {
49
+ walkDomainConfig(node, `domain-config[${index}]`, 0, [], baseScope?.trustAnchorsChain ?? [], baseScope?.cleartextTrafficPermitted, domainScopes);
50
+ });
51
+ return { baseScope, domainScopes, debugTrustAnchors: parsed.debugOverrides?.trustAnchors };
52
+ }
@@ -0,0 +1,14 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 2 — public exports for the standalone Android network-
3
+ // security (cleartext traffic / Network Security Config) audit.
4
+ //
5
+ // Exports only what a later integration batch (or a test) needs: the parser,
6
+ // the effective-policy deriver, the analyzer, and the standalone check-result
7
+ // builder. Internal helpers (attribute parsing, bounded previews, per-scope
8
+ // finding builders) stay unexported.
9
+ // ---------------------------------------------------------------------------
10
+ export { parseNetworkSecurityConfig } from "./parseNetworkSecurityConfig.js";
11
+ export { deriveEffectiveNetworkPolicy } from "./deriveEffectiveNetworkPolicy.js";
12
+ export { extractManifestNetworkSecurityEvidence } from "./manifestEvidence.js";
13
+ export { analyzeManifestNetworkSecurity } from "./analyzeNetworkSecurity.js";
14
+ export { auditAndroidNetworkSecurity, ANDROID_NETWORK_SECURITY_AUDIT_CHECK_ID } from "./checkResult.js";
@@ -0,0 +1,109 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { parseAndroidResourceReference } from "../resourceReference.js";
4
+ import { resolveAndroidXmlResourceReference } from "../resourceResolution.js";
5
+ import { parseNetworkSecurityConfig } from "./parseNetworkSecurityConfig.js";
6
+ function classifyUsesCleartextTraffic(entry) {
7
+ const app = entry.manifest.application;
8
+ if (app.usesCleartextTraffic === true)
9
+ return { state: "explicit-true", raw: app.usesCleartextTrafficRaw ?? "true" };
10
+ if (app.usesCleartextTraffic === false)
11
+ return { state: "explicit-false", raw: app.usesCleartextTrafficRaw ?? "false" };
12
+ if (app.usesCleartextTrafficRaw !== undefined)
13
+ return { state: "malformed", raw: app.usesCleartextTrafficRaw };
14
+ return { state: "missing" };
15
+ }
16
+ function parseNetworkSecurityConfigFile(absolutePath) {
17
+ let xmlText;
18
+ try {
19
+ xmlText = readFileSync(absolutePath, "utf8");
20
+ }
21
+ catch (error) {
22
+ return { state: "malformed-xml", reason: error instanceof Error ? error.message : "Unable to read the resolved Network Security Config file" };
23
+ }
24
+ return parseNetworkSecurityConfig(xmlText);
25
+ }
26
+ function classifyResolution(raw, resolution, targetRoot) {
27
+ switch (resolution.state) {
28
+ case "resolved": {
29
+ const candidate = resolution.candidates[0];
30
+ return {
31
+ state: "resolved",
32
+ raw,
33
+ modulePath: resolution.modulePath,
34
+ candidate,
35
+ parseResult: parseNetworkSecurityConfigFile(path.join(targetRoot, candidate.relativePath)),
36
+ };
37
+ }
38
+ case "ambiguous":
39
+ return {
40
+ state: "ambiguous",
41
+ raw,
42
+ modulePath: resolution.modulePath,
43
+ candidates: resolution.candidates.map((candidate) => ({
44
+ candidate,
45
+ parseResult: parseNetworkSecurityConfigFile(path.join(targetRoot, candidate.relativePath)),
46
+ })),
47
+ };
48
+ case "missing":
49
+ return { state: "missing", raw, modulePath: resolution.modulePath, searchedSourceSets: resolution.searchedSourceSets };
50
+ case "malformed-reference":
51
+ return { state: "malformed", raw, reason: resolution.reason };
52
+ case "unsupported-reference":
53
+ // Unreachable in practice: this function only calls the resolver once
54
+ // parseAndroidResourceReference has already confirmed a "parsed"
55
+ // (xml-type) reference, so the resolver's own unsupported-reference
56
+ // branch (package-qualified/non-xml) cannot trigger from this path.
57
+ // Handled defensively rather than asserted unreachable.
58
+ return {
59
+ state: "unsupported-type",
60
+ raw,
61
+ resourceType: "type" in resolution.parsed ? resolution.parsed.type : "unknown",
62
+ };
63
+ }
64
+ }
65
+ // Extracts and classifies cleartext-traffic/Network Security Config evidence
66
+ // for one already-parsed manifest. `targetRoot` must be the absolute Android
67
+ // target root (the same root the manifest/resource resolver operate within).
68
+ export function extractManifestNetworkSecurityEvidence(targetRoot, entry) {
69
+ const usesCleartextTraffic = classifyUsesCleartextTraffic(entry);
70
+ const raw = entry.manifest.application.networkSecurityConfigRef;
71
+ let networkSecurityConfig;
72
+ if (raw === undefined) {
73
+ networkSecurityConfig = { state: "absent" };
74
+ }
75
+ else {
76
+ const parsedRef = parseAndroidResourceReference(raw);
77
+ if (parsedRef.state === "placeholder") {
78
+ networkSecurityConfig = { state: "placeholder", raw };
79
+ }
80
+ else if (parsedRef.state === "malformed") {
81
+ networkSecurityConfig = { state: "malformed", raw, reason: parsedRef.reason };
82
+ }
83
+ else if (parsedRef.state === "empty") {
84
+ networkSecurityConfig = { state: "malformed", raw, reason: "Reference is empty" };
85
+ }
86
+ else if (parsedRef.state === "unsupported-type") {
87
+ networkSecurityConfig = { state: "unsupported-type", raw, resourceType: parsedRef.type };
88
+ }
89
+ else if (parsedRef.state === "package-qualified") {
90
+ networkSecurityConfig = { state: "package-qualified", raw, packageQualifier: parsedRef.packageQualifier };
91
+ }
92
+ else if (entry.modulePath === undefined) {
93
+ networkSecurityConfig = { state: "module-unknown", raw };
94
+ }
95
+ else {
96
+ const moduleAbsolutePath = path.join(targetRoot, entry.modulePath);
97
+ const resolution = resolveAndroidXmlResourceReference(targetRoot, moduleAbsolutePath, raw);
98
+ networkSecurityConfig = classifyResolution(raw, resolution, targetRoot);
99
+ }
100
+ }
101
+ return {
102
+ manifestPath: entry.manifestPath,
103
+ modulePath: entry.modulePath,
104
+ sourceSetKind: entry.sourceSetKind,
105
+ applicationLocation: entry.manifest.application.location,
106
+ usesCleartextTraffic,
107
+ networkSecurityConfig,
108
+ };
109
+ }
@@ -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
+ }