@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,116 @@
1
+ import { makeAndroidFinding } from "../../audit/androidFinding.js";
2
+ import { buildAndroidSourceLocation } from "../sourceLocation.js";
3
+ import { makeCandidateEvidence } from "../candidateEvidence.js";
4
+ import { fingerprintCandidateValue, redactedPreviewForCandidate } from "../redaction.js";
5
+ import { matchPrivateKeyBlocks, matchSensitiveAssignments } from "./matchSecretCandidates.js";
6
+ import { isReferenceSyntax, shouldSuppressEntirely } from "./suppressValue.js";
7
+ function fileKindFor(relativePath) {
8
+ const lower = relativePath.toLowerCase();
9
+ if (lower.endsWith(".kt") || lower.endsWith(".kts") || lower.endsWith(".java") || lower.endsWith(".gradle") || lower.endsWith(".gradle.kts")) {
10
+ return "code";
11
+ }
12
+ if (lower.endsWith(".properties") || lower.endsWith(".yaml") || lower.endsWith(".yml") || lower.endsWith(".toml")) {
13
+ return "config-unquoted";
14
+ }
15
+ // .xml/.json use quoted-form matching (JSON always quotes; XML attribute
16
+ // values are quoted by construction).
17
+ return "config-quotable";
18
+ }
19
+ function classifyIdentifier(identifier) {
20
+ const normalized = identifier.toLowerCase().replace(/[-_]/g, "");
21
+ if (["storepassword", "keypassword", "signingpassword"].includes(normalized)) {
22
+ return { ruleId: "android-signing-password-literal", sensitiveDataCategory: "signing-password" };
23
+ }
24
+ if (["databasepassword", "dbpassword"].includes(normalized)) {
25
+ return { ruleId: "android-secret-token-password-candidate", sensitiveDataCategory: "database-credential" };
26
+ }
27
+ if (["password", "passwd", "pwd"].includes(normalized)) {
28
+ return { ruleId: "android-secret-token-password-candidate", sensitiveDataCategory: "password" };
29
+ }
30
+ if (normalized === "clientsecret") {
31
+ return { ruleId: "android-secret-token-password-candidate", sensitiveDataCategory: "oauth-client-secret" };
32
+ }
33
+ if (normalized === "accesstoken") {
34
+ return { ruleId: "android-secret-token-password-candidate", sensitiveDataCategory: "access-token" };
35
+ }
36
+ if (normalized === "bearertoken") {
37
+ return { ruleId: "android-secret-token-password-candidate", sensitiveDataCategory: "bearer-token" };
38
+ }
39
+ if (normalized === "authtoken") {
40
+ return { ruleId: "android-secret-token-password-candidate", sensitiveDataCategory: "access-token" };
41
+ }
42
+ if (normalized === "apisecret") {
43
+ return { ruleId: "android-secret-token-password-candidate", sensitiveDataCategory: "cloud-secret-key" };
44
+ }
45
+ // Bare "secret" and any other sensitive-pattern match not covered above.
46
+ return { ruleId: "android-secret-hardcoded-candidate", sensitiveDataCategory: "unknown-sensitive-value" };
47
+ }
48
+ export function analyzeSecretCandidateFile(targetRoot, file) {
49
+ const candidates = [];
50
+ const findings = [];
51
+ const fileKind = fileKindFor(file.relativePath);
52
+ for (const block of matchPrivateKeyBlocks(file.content)) {
53
+ const location = buildAndroidSourceLocation(targetRoot, file.absolutePath, { line: block.line });
54
+ const fingerprint = fingerprintCandidateValue(block.rawBlock);
55
+ if (!block.terminated) {
56
+ candidates.push(makeCandidateEvidence({
57
+ ruleId: "android-secret-private-key-candidate",
58
+ category: "android-secret-candidates",
59
+ confidence: "medium",
60
+ modulePath: file.modulePath,
61
+ location,
62
+ summary: `Unterminated ${block.headerLabel} block found; treated conservatively`,
63
+ rawValue: block.rawBlock,
64
+ resolutionState: "malformed",
65
+ staticAnalysisLimitations: ["Block was not terminated by a matching END marker within the bounded scan window; content was not validated as a real key."],
66
+ }));
67
+ continue;
68
+ }
69
+ findings.push(makeAndroidFinding({
70
+ ruleId: "android-secret-private-key-candidate",
71
+ title: `Private key material found (${block.headerLabel})`,
72
+ severity: "major",
73
+ confidence: "high",
74
+ description: `A ${block.headerLabel} block was found in target-contained text. This is high-confidence structural evidence of committed private key material; the key was not opened, parsed, or validated.`,
75
+ manifestPath: file.relativePath,
76
+ identity: fingerprint,
77
+ location: { line: block.line },
78
+ evidenceDetails: [`redactedPreview=${redactedPreviewForCandidate(block.rawBlock, { maxLength: 40 })}`],
79
+ recommendation: "Remove the private key from source control immediately and rotate the corresponding credential/certificate.",
80
+ }));
81
+ }
82
+ for (const match of matchSensitiveAssignments(file.content, fileKind)) {
83
+ if (shouldSuppressEntirely(match.rawValue))
84
+ continue;
85
+ const location = buildAndroidSourceLocation(targetRoot, file.absolutePath, { line: match.line });
86
+ const classification = classifyIdentifier(match.identifier);
87
+ if (isReferenceSyntax(match.rawValue)) {
88
+ candidates.push(makeCandidateEvidence({
89
+ ruleId: classification.ruleId,
90
+ category: classification.ruleId === "android-signing-password-literal" ? "android-signing-configuration" : "android-secret-candidates",
91
+ confidence: "low",
92
+ modulePath: file.modulePath,
93
+ location,
94
+ summary: `"${match.identifier}" is assigned an environment/property/placeholder reference, not a literal value`,
95
+ rawValue: match.rawValue,
96
+ resolutionState: "not-applicable",
97
+ staticAnalysisLimitations: ["Reference syntax was detected; the resolved value was not evaluated and is not treated as a hardcoded credential."],
98
+ }));
99
+ continue;
100
+ }
101
+ const fingerprint = fingerprintCandidateValue(match.rawValue);
102
+ findings.push(makeAndroidFinding({
103
+ ruleId: classification.ruleId,
104
+ title: `Hardcoded ${match.identifier} literal found`,
105
+ severity: "major",
106
+ confidence: "high",
107
+ description: `The identifier "${match.identifier}" is assigned a literal, non-placeholder value. This is high-confidence static evidence of a hardcoded credential; it is not proof the credential is currently valid or in active use.`,
108
+ manifestPath: file.relativePath,
109
+ identity: fingerprint,
110
+ location: { line: match.line },
111
+ evidenceDetails: [`identifier=${match.identifier}`, `redactedPreview=${redactedPreviewForCandidate(match.rawValue)}`],
112
+ recommendation: "Remove the hardcoded credential, rotate it, and load it from a secure secrets mechanism instead.",
113
+ }));
114
+ }
115
+ return { candidates, findings };
116
+ }
@@ -0,0 +1,80 @@
1
+ import { sortCandidateEvidence } from "../ordering.js";
2
+ import { discoverSecretSourceFiles } from "./discoverSecretSourceFiles.js";
3
+ import { analyzeSecretCandidateFile } from "./analyzeSecretCandidates.js";
4
+ // ---------------------------------------------------------------------------
5
+ // v0.4.1 Batch 4 — standalone Android secret-candidate check result.
6
+ //
7
+ // Operates over discovered files rather than parsed manifests, so it cannot
8
+ // reuse buildAndroidManifestCheckResult (Batch 3's manifest-audit builder) —
9
+ // this is a distinct, still-conservative status-decision policy: never-run
10
+ // for a non-Android target, "failed" only when a reportable (non-
11
+ // informational) finding exists, otherwise "passed". Standalone: not called
12
+ // from validateAndroidTarget / any active orchestration, not rendered in
13
+ // reports, no CLI effect. Batch 8 integrates.
14
+ // ---------------------------------------------------------------------------
15
+ export const ANDROID_SECRET_CANDIDATES_AUDIT_CHECK_ID = "android-secret-candidates-audit";
16
+ export function auditAndroidSecretCandidates(targetRoot, detection) {
17
+ if (detection.projectKind === "non-android") {
18
+ return {
19
+ id: ANDROID_SECRET_CANDIDATES_AUDIT_CHECK_ID,
20
+ category: "android-secret-candidates",
21
+ title: "Android hardcoded secret-candidate audit",
22
+ status: "unsupported",
23
+ requirementLevel: "optional",
24
+ ran: false,
25
+ skipped: true,
26
+ skipInfo: {
27
+ checkId: ANDROID_SECRET_CANDIDATES_AUDIT_CHECK_ID,
28
+ reason: "Target was not detected as an Android project.",
29
+ requirementLevel: "optional",
30
+ missingCapability: "android-detection",
31
+ verdictImpact: "does not apply to a non-Android target",
32
+ recommendedNextAction: "Re-run against an Android Gradle project, or verify project detection.",
33
+ },
34
+ evidence: [],
35
+ findings: [],
36
+ warnings: [],
37
+ errors: [],
38
+ sourcePaths: [],
39
+ confidence: "unknown",
40
+ environmentRequirements: [],
41
+ candidateEvidence: [],
42
+ };
43
+ }
44
+ const modulePaths = detection.modules.map((m) => m.path);
45
+ const { files, skipped } = discoverSecretSourceFiles(targetRoot, modulePaths);
46
+ const findingsById = new Map();
47
+ const candidatesById = new Map();
48
+ for (const file of files) {
49
+ const result = analyzeSecretCandidateFile(targetRoot, file);
50
+ for (const finding of result.findings)
51
+ findingsById.set(finding.id, finding);
52
+ for (const candidate of result.candidates)
53
+ candidatesById.set(candidate.id, candidate);
54
+ }
55
+ const findings = [...findingsById.values()].sort((a, b) => a.id.localeCompare(b.id));
56
+ const candidateEvidence = sortCandidateEvidence([...candidatesById.values()]);
57
+ const evidence = [`${files.length} file(s) scanned across ${new Set(files.map((f) => f.modulePath ?? "(root)")).size} module scope(s)`];
58
+ const warnings = skipped.map((s) => `${s.relativePath}: skipped (${s.reason}${s.detail ? `: ${s.detail}` : ""})`);
59
+ const hasReportableFinding = findings.some((f) => f.severity !== "informational");
60
+ return {
61
+ id: ANDROID_SECRET_CANDIDATES_AUDIT_CHECK_ID,
62
+ category: "android-secret-candidates",
63
+ title: "Android hardcoded secret-candidate audit",
64
+ status: hasReportableFinding ? "failed" : "passed",
65
+ requirementLevel: "required",
66
+ ran: true,
67
+ skipped: false,
68
+ evidence,
69
+ findings,
70
+ warnings: [
71
+ ...warnings,
72
+ "Static analysis only: does not validate credentials, contact external services, or scan Git history.",
73
+ ],
74
+ errors: [],
75
+ sourcePaths: files.map((f) => f.relativePath),
76
+ confidence: skipped.length > 0 ? "medium" : "high",
77
+ environmentRequirements: [],
78
+ candidateEvidence,
79
+ };
80
+ }
@@ -0,0 +1,152 @@
1
+ import fs from "node:fs";
2
+ import { collectFilesForGlobs } from "../../../../core/fileGlobs.js";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.4.1 Batch 4 — bounded, deterministic file discovery for the secret-
5
+ // candidate scanner.
6
+ //
7
+ // Reuses the existing target-contained glob collector
8
+ // (src/core/fileGlobs.ts's collectFilesForGlobs, which itself uses
9
+ // src/core/pathSafety.ts for containment) for traversal — no second
10
+ // traversal engine. Adds narrow, documented bounds (size cap, binary
11
+ // detection) co-located here because no existing shared utility exposes
12
+ // structured skip evidence (the closest existing owner,
13
+ // src/securityValidation/attackScenarios/boundedSourceScan.ts, silently
14
+ // drops oversized/unreadable/binary files with no skip record, and is owned
15
+ // by an unrelated security-validation feature, not Android).
16
+ // ---------------------------------------------------------------------------
17
+ // Extension-based recursive globs plus explicit root-level literals — the
18
+ // existing glob matcher's "**/*.ext" form requires at least one path
19
+ // separator, so genuinely root-level files (e.g. local.properties sitting
20
+ // directly in the target root) need an explicit literal entry too.
21
+ export const SECRET_SCAN_GLOBS = [
22
+ "**/*.kt",
23
+ "**/*.kts",
24
+ "**/*.java",
25
+ "**/*.xml",
26
+ "**/*.gradle",
27
+ "**/*.gradle.kts",
28
+ "**/*.properties",
29
+ "**/*.json",
30
+ "**/*.yaml",
31
+ "**/*.yml",
32
+ "**/*.toml",
33
+ "gradle.properties",
34
+ "local.properties",
35
+ "build.gradle",
36
+ "build.gradle.kts",
37
+ "settings.gradle",
38
+ "settings.gradle.kts",
39
+ ];
40
+ // Path segments excluded beyond src/core/fileGlobs.ts's own directory
41
+ // exclusions (node_modules/dist/build/coverage/.git/lab-output/.my-dev-kit),
42
+ // matching the wider Android-appropriate set already used by
43
+ // src/mobile/android/detect/traversal.ts.
44
+ const EXCLUDED_PATH_SEGMENTS = [".gradle/", "out/", "reports/", ".idea/", ".vscode/", ".vs/"];
45
+ function isExcludedPath(relativePath) {
46
+ const normalized = `${relativePath.replace(/\\/g, "/")}/`;
47
+ return EXCLUDED_PATH_SEGMENTS.some((segment) => normalized === segment || normalized.startsWith(segment) || normalized.includes(`/${segment}`));
48
+ }
49
+ // Documented, co-located bounds (no existing shared constant applies here).
50
+ export const MAX_SECRET_SCAN_FILE_BYTES = 1_000_000;
51
+ export const MAX_SECRET_SCAN_FILE_COUNT = 5_000;
52
+ const NUL_CHAR = String.fromCharCode(0);
53
+ function inferModulePath(relativePath, knownModulePaths) {
54
+ const normalized = relativePath.replace(/\\/g, "/");
55
+ let best;
56
+ for (const modulePath of knownModulePaths) {
57
+ const prefix = `${modulePath.replace(/\\/g, "/")}/`;
58
+ if (normalized.startsWith(prefix) && (best === undefined || modulePath.length > best.length)) {
59
+ best = modulePath;
60
+ }
61
+ }
62
+ return best;
63
+ }
64
+ // Discovers a bounded, deterministic set of target-contained text files
65
+ // eligible for secret-candidate scanning. Never throws for missing glob base
66
+ // paths (per-glob isolation, same rationale as boundedSourceScan.ts), never
67
+ // reads beyond the size cap, and reports every skip with a structured
68
+ // reason rather than silently dropping files.
69
+ export function discoverSecretSourceFiles(targetRoot, knownModulePaths = []) {
70
+ const candidateMap = new Map();
71
+ for (const glob of SECRET_SCAN_GLOBS) {
72
+ let matches;
73
+ try {
74
+ matches = collectFilesForGlobs(targetRoot, [glob]);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ for (const match of matches) {
80
+ const normalizedRel = match.relativePath.replace(/\\/g, "/");
81
+ if (isExcludedPath(normalizedRel))
82
+ continue;
83
+ candidateMap.set(normalizedRel, match.absolutePath);
84
+ }
85
+ }
86
+ const candidates = [...candidateMap.entries()]
87
+ .sort(([a], [b]) => a.localeCompare(b))
88
+ .slice(0, MAX_SECRET_SCAN_FILE_COUNT)
89
+ .map(([relativePath, absolutePath]) => ({ relativePath, absolutePath }));
90
+ const files = [];
91
+ const skipped = [];
92
+ for (const candidate of candidates) {
93
+ let stat;
94
+ try {
95
+ stat = fs.lstatSync(candidate.absolutePath);
96
+ }
97
+ catch (error) {
98
+ skipped.push({ relativePath: candidate.relativePath, reason: "unreadable", detail: error instanceof Error ? error.message : undefined });
99
+ continue;
100
+ }
101
+ // Symlinks are not followed outside the target: resolve the real path
102
+ // and re-check containment before ever reading content.
103
+ let realAbsolutePath = candidate.absolutePath;
104
+ if (stat.isSymbolicLink()) {
105
+ try {
106
+ realAbsolutePath = fs.realpathSync(candidate.absolutePath);
107
+ }
108
+ catch {
109
+ skipped.push({ relativePath: candidate.relativePath, reason: "unreadable", detail: "broken symlink" });
110
+ continue;
111
+ }
112
+ const normalizedRoot = targetRoot.replace(/\\/g, "/");
113
+ const normalizedReal = realAbsolutePath.replace(/\\/g, "/");
114
+ if (!normalizedReal.startsWith(normalizedRoot)) {
115
+ skipped.push({ relativePath: candidate.relativePath, reason: "unreadable", detail: "symlink escapes target root" });
116
+ continue;
117
+ }
118
+ try {
119
+ stat = fs.statSync(realAbsolutePath);
120
+ }
121
+ catch (error) {
122
+ skipped.push({ relativePath: candidate.relativePath, reason: "unreadable", detail: error instanceof Error ? error.message : undefined });
123
+ continue;
124
+ }
125
+ }
126
+ if (!stat.isFile())
127
+ continue;
128
+ if (stat.size > MAX_SECRET_SCAN_FILE_BYTES) {
129
+ skipped.push({ relativePath: candidate.relativePath, reason: "oversized", detail: `${stat.size} bytes exceeds ${MAX_SECRET_SCAN_FILE_BYTES}` });
130
+ continue;
131
+ }
132
+ let content;
133
+ try {
134
+ content = fs.readFileSync(realAbsolutePath, "utf8");
135
+ }
136
+ catch (error) {
137
+ skipped.push({ relativePath: candidate.relativePath, reason: "unreadable", detail: error instanceof Error ? error.message : undefined });
138
+ continue;
139
+ }
140
+ if (content.indexOf(NUL_CHAR) !== -1) {
141
+ skipped.push({ relativePath: candidate.relativePath, reason: "binary-like" });
142
+ continue;
143
+ }
144
+ files.push({
145
+ relativePath: candidate.relativePath,
146
+ absolutePath: candidate.absolutePath,
147
+ modulePath: inferModulePath(candidate.relativePath, knownModulePaths),
148
+ content,
149
+ });
150
+ }
151
+ return { files, skipped };
152
+ }
@@ -0,0 +1,9 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 4 — public exports for the standalone Android secret-
3
+ // candidate audit. Deliberately does not export the raw matcher
4
+ // (matchSecretCandidates.ts), suppression internals, or raw match types —
5
+ // only the discovery/analysis/check-result layers that already redact.
6
+ // ---------------------------------------------------------------------------
7
+ export { discoverSecretSourceFiles, SECRET_SCAN_GLOBS, MAX_SECRET_SCAN_FILE_BYTES, MAX_SECRET_SCAN_FILE_COUNT } from "./discoverSecretSourceFiles.js";
8
+ export { analyzeSecretCandidateFile } from "./analyzeSecretCandidates.js";
9
+ export { auditAndroidSecretCandidates, ANDROID_SECRET_CANDIDATES_AUDIT_CHECK_ID } from "./checkResult.js";
@@ -0,0 +1,136 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 4 — narrow, deterministic in-memory secret-candidate matching.
3
+ //
4
+ // This is the ONLY layer that ever holds a raw candidate value. Callers
5
+ // (analyzeSecretCandidates.ts) must redact/fingerprint immediately and never
6
+ // store, log, or export the raw value from this module. Nothing in this
7
+ // file is exported through index.ts.
8
+ //
9
+ // No entropy scoring is implemented: the structural markers (PEM headers)
10
+ // and explicit sensitive-identifier context already meet the "high
11
+ // confidence" bar the batch requires without it, and entropy alone must
12
+ // never create a finding per agents.txt Batch 4 section 9.8 — omitting it
13
+ // avoids speculative complexity with no corresponding required rule.
14
+ // ---------------------------------------------------------------------------
15
+ const PRIVATE_KEY_HEADER_PATTERN = /-----BEGIN ([A-Z0-9 ]*?PRIVATE KEY)-----/g;
16
+ function findBlockEnd(content, searchStart, headerLabel) {
17
+ const footer = `-----END ${headerLabel}-----`;
18
+ const footerIndex = content.indexOf(footer, searchStart);
19
+ if (footerIndex === -1) {
20
+ // Unterminated block — bound the captured region rather than reading to
21
+ // end of file unbounded.
22
+ const MAX_UNTERMINATED_BLOCK_LENGTH = 4000;
23
+ return { end: Math.min(content.length, searchStart + MAX_UNTERMINATED_BLOCK_LENGTH), terminated: false };
24
+ }
25
+ return { end: footerIndex + footer.length, terminated: true };
26
+ }
27
+ function lineNumberAt(content, index) {
28
+ let line = 1;
29
+ for (let i = 0; i < index && i < content.length; i++) {
30
+ if (content[i] === "\n")
31
+ line++;
32
+ }
33
+ return line;
34
+ }
35
+ // Public keys and certificates ("-----BEGIN PUBLIC KEY-----",
36
+ // "-----BEGIN CERTIFICATE-----") never match PRIVATE_KEY_HEADER_PATTERN
37
+ // (which requires the literal substring "PRIVATE KEY") — the boundary is
38
+ // enforced by construction, not a separate exclusion check.
39
+ export function matchPrivateKeyBlocks(content) {
40
+ const matches = [];
41
+ for (const headerMatch of content.matchAll(PRIVATE_KEY_HEADER_PATTERN)) {
42
+ const headerLabel = headerMatch[1];
43
+ const startIndex = headerMatch.index ?? 0;
44
+ const { end, terminated } = findBlockEnd(content, startIndex, headerLabel);
45
+ matches.push({
46
+ line: lineNumberAt(content, startIndex),
47
+ headerLabel,
48
+ rawBlock: content.slice(startIndex, end),
49
+ terminated,
50
+ });
51
+ }
52
+ return matches;
53
+ }
54
+ // Sensitive identifier names that, when paired with a literal (never a
55
+ // reference/placeholder), are treated as high-confidence secret-shaped
56
+ // evidence. Deliberately narrow — matches agents.txt Batch 4 section 9.5.
57
+ //
58
+ // Splits the identifier into words (snake_case and camelCase boundaries)
59
+ // rather than using a single \b-anchored regex, so real-world constant
60
+ // naming like `SECRET_TOKEN`, `DB_PASSWORD`, or `placeholderSecret` is
61
+ // recognized — a plain `\bsecret\b` regex fails on these because
62
+ // underscore and camelCase transitions are not `\b` word boundaries.
63
+ const SENSITIVE_SINGLE_WORDS = new Set(["password", "passwd", "pwd", "secret"]);
64
+ const SENSITIVE_WORD_PAIRS = [
65
+ ["client", "secret"],
66
+ ["access", "token"],
67
+ ["bearer", "token"],
68
+ ["auth", "token"],
69
+ ["api", "secret"],
70
+ ["signing", "password"],
71
+ ["store", "password"],
72
+ ["key", "password"],
73
+ ["database", "password"],
74
+ ["db", "password"],
75
+ ];
76
+ function splitIdentifierWords(identifier) {
77
+ return identifier
78
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
79
+ .split(/[_\-.\s]+/)
80
+ .map((word) => word.toLowerCase())
81
+ .filter((word) => word.length > 0);
82
+ }
83
+ function isSensitiveIdentifier(identifier) {
84
+ const words = splitIdentifierWords(identifier);
85
+ if (words.some((word) => SENSITIVE_SINGLE_WORDS.has(word)))
86
+ return true;
87
+ for (const [first, second] of SENSITIVE_WORD_PAIRS) {
88
+ for (let i = 0; i < words.length - 1; i++) {
89
+ if (words[i] === first && words[i + 1] === second)
90
+ return true;
91
+ }
92
+ }
93
+ return false;
94
+ }
95
+ // Finds `identifier = "value"` / `identifier: "value"` / `identifier="value"`
96
+ // / `"identifier": "value"` (JSON-style quoted key) pairs anywhere on a line
97
+ // (the value side quoting is required — code files never match an unquoted
98
+ // right-hand side, so a variable/function reference such as
99
+ // `val password = getPassword()` can never be mistaken for a literal).
100
+ const KEY_VALUE_QUOTED_PATTERN = /["']?([A-Za-z_][A-Za-z0-9_]*)["']?\s*[:=]\s*["']([^"']*)["']/g;
101
+ // Properties/YAML/TOML permit unquoted scalar values on a `key = value` /
102
+ // `key: value` line; still requires the key itself to match the sensitive
103
+ // identifier pattern.
104
+ const KEY_VALUE_UNQUOTED_PATTERN = /^\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*[:=]\s*(\S.*?)\s*$/;
105
+ // Scans line-by-line (bounds work naturally; also gives exact line numbers
106
+ // without a second pass). Returns only pairs whose *captured identifier*
107
+ // (not just the line generally) matches a sensitive name — reference/
108
+ // placeholder/empty-value filtering happens in analyzeSecretCandidates.ts,
109
+ // which also owns redaction, so this function's return values are the last
110
+ // point at which a raw value exists before that immediate conversion.
111
+ export function matchSensitiveAssignments(content, fileKind) {
112
+ const matches = [];
113
+ const lines = content.split(/\r\n|\r|\n/);
114
+ for (let i = 0; i < lines.length; i++) {
115
+ const line = lines[i];
116
+ if (fileKind === "config-unquoted") {
117
+ const unquoted = line.match(KEY_VALUE_UNQUOTED_PATTERN);
118
+ if (unquoted && isSensitiveIdentifier(unquoted[1])) {
119
+ matches.push({ line: i + 1, identifier: unquoted[1], rawValue: stripSurroundingQuotes(unquoted[2]) });
120
+ }
121
+ continue;
122
+ }
123
+ for (const pairMatch of line.matchAll(KEY_VALUE_QUOTED_PATTERN)) {
124
+ if (isSensitiveIdentifier(pairMatch[1])) {
125
+ matches.push({ line: i + 1, identifier: pairMatch[1], rawValue: pairMatch[2] });
126
+ }
127
+ }
128
+ }
129
+ return matches;
130
+ }
131
+ function stripSurroundingQuotes(value) {
132
+ if (value.length >= 2 && ((value[0] === '"' && value.endsWith('"')) || (value[0] === "'" && value.endsWith("'")))) {
133
+ return value.slice(1, -1);
134
+ }
135
+ return value;
136
+ }
@@ -0,0 +1,61 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 4 — conservative placeholder/example/reference suppression.
3
+ //
4
+ // Deliberately a small, exact/narrow-normalized check list rather than a
5
+ // large heuristic catalog (agents.txt Batch 4 section 9.9). Operates only on
6
+ // the already-captured raw value (never re-reads the file), and this module
7
+ // is not exported through index.ts.
8
+ // ---------------------------------------------------------------------------
9
+ const EXACT_PLACEHOLDER_VALUES = new Set([
10
+ "changeme",
11
+ "change_me",
12
+ "change-me",
13
+ "password",
14
+ "secret",
15
+ "example",
16
+ "sample",
17
+ "dummy",
18
+ "fake",
19
+ "test",
20
+ "your-token-here",
21
+ "your_token_here",
22
+ "replace-me",
23
+ "replace_me",
24
+ "redacted",
25
+ "masked",
26
+ "todo",
27
+ "xxx",
28
+ "placeholder",
29
+ "n/a",
30
+ "none",
31
+ ].map((v) => v.toLowerCase()));
32
+ export function isEmptyValue(value) {
33
+ return value.trim().length === 0;
34
+ }
35
+ export function isObviousPlaceholderValue(value) {
36
+ const normalized = value.trim().toLowerCase();
37
+ if (normalized.length === 0)
38
+ return true;
39
+ if (EXACT_PLACEHOLDER_VALUES.has(normalized))
40
+ return true;
41
+ // Fully repeated single character (e.g. "****", "xxxxxxxx", "00000000").
42
+ if (/^(.)\1*$/.test(normalized))
43
+ return true;
44
+ return false;
45
+ }
46
+ // Environment/property/manifest-placeholder reference syntax embedded in the
47
+ // literal itself (e.g. a Kotlin string template `"${DB_PASSWORD}"`, a
48
+ // properties-file `${some.property}`, a shell-style `$VAR`, a Windows
49
+ // `%VAR%`, or an Android manifest `@string/...`/`@xml/...` reference). Any
50
+ // of these means the "literal" is not actually a hardcoded value.
51
+ const REFERENCE_SYNTAX_PATTERN = /\$\{[^}]*\}|\$[A-Za-z_][A-Za-z0-9_]*|%[A-Za-z_][A-Za-z0-9_]*%|^@(string|xml|raw|array|integer|bool)\//;
52
+ export function isReferenceSyntax(value) {
53
+ return REFERENCE_SYNTAX_PATTERN.test(value.trim());
54
+ }
55
+ // True when the value should never become a finding/candidate at all —
56
+ // empty or an obvious placeholder/example. Reference syntax is handled
57
+ // separately by the caller because it should surface as non-literal
58
+ // metadata (Batch 4 section 9.12), not be silently dropped.
59
+ export function shouldSuppressEntirely(value) {
60
+ return isEmptyValue(value) || isObviousPlaceholderValue(value);
61
+ }
@@ -0,0 +1,11 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 4 — bounded secret-candidate scan contracts.
3
+ //
4
+ // Local to this feature; does not duplicate Batch 1's CandidateEvidence,
5
+ // SecurityFinding, or the shared bounded-source-scan utility
6
+ // (src/securityValidation/attackScenarios/boundedSourceScan.ts) — it wraps
7
+ // the existing target-containment/glob-collection primitives
8
+ // (src/core/fileGlobs.ts) with Android-appropriate exclusions and adds
9
+ // structured skip evidence, which neither existing utility provides.
10
+ // ---------------------------------------------------------------------------
11
+ export {};
@@ -0,0 +1,34 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 1 — sensitive-data category vocabulary for later secret/
3
+ // signing/configuration candidate scanners.
4
+ //
5
+ // Deliberately small and additive. This is not a security-severity scale
6
+ // (see SecuritySeverity in src/securityValidation/types.ts, reused as-is by
7
+ // candidateEvidence.ts) — it only classifies *what kind* of value a static
8
+ // candidate looks like.
9
+ //
10
+ // A bare Firebase API key (the public `apiKey` field in google-services.json
11
+ // / Firebase web config) must NOT be classified as "cloud-secret-key" or any
12
+ // other private-secret category by default: Firebase API keys identify a
13
+ // Firebase project but are not treated as confidential by Google's own
14
+ // documentation, and misclassifying them would produce a permanent, noisy
15
+ // false positive for a huge fraction of real Android apps. A later batch's
16
+ // Firebase scanner should classify that value as "unknown-sensitive-value"
17
+ // (or omit a candidate entirely) rather than a private-secret category.
18
+ // ---------------------------------------------------------------------------
19
+ export const SENSITIVE_DATA_CATEGORIES = [
20
+ "password",
21
+ "access-token",
22
+ "bearer-token",
23
+ "oauth-client-secret",
24
+ "cloud-secret-key",
25
+ "private-key",
26
+ "database-credential",
27
+ "signing-password",
28
+ "recovery-code",
29
+ "personal-data",
30
+ "unknown-sensitive-value",
31
+ ];
32
+ export function isSensitiveDataCategory(value) {
33
+ return SENSITIVE_DATA_CATEGORIES.includes(value);
34
+ }