@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.
- package/README.md +269 -47
- package/dist/scripts/audits/runAudit.js +28 -1
- package/dist/scripts/security/validate.js +38 -5
- package/dist/src/audits/core/auditConfig.js +16 -1
- package/dist/src/audits/core/auditRunner.js +6 -2
- package/dist/src/audits/report/auditReportModel.js +7 -0
- package/dist/src/audits/report/renderAuditJsonReport.js +1 -0
- package/dist/src/audits/report/renderAuditTextReport.js +37 -0
- package/dist/src/audits/security/androidAuditIntegration.js +145 -0
- package/dist/src/audits/security/mapAndroidSecurityFindingToAuditIssue.js +116 -0
- package/dist/src/audits/security/mapSecurityFindingToAuditIssue.js +15 -5
- package/dist/src/audits/security/securityAuditAdapter.js +20 -2
- package/dist/src/audits/security/securityAuditTypes.js +37 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
- package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
- package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
- package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
- package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
- package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
- package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
- package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
- package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
- package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
- package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
- package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +123 -17
- package/dist/src/mobile/android/manifest/parseAndroidManifest.js +27 -4
- package/dist/src/mobile/android/manifest/xml/parseXml.js +25 -9
- package/dist/src/mobile/android/report/model.js +32 -1
- package/dist/src/mobile/android/report/renderAndroidReport.js +63 -0
- package/dist/src/mobile/android/validate/validateAndroidTarget.js +146 -3
- package/dist/src/mobile/android/validation/checkResult.js +7 -0
- package/dist/src/securityValidation/commandRunner.js +10 -1
- package/dist/src/securityValidation/validate/cliOptions.js +46 -0
- package/package.json +1 -1
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
- package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
- package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
|
@@ -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
|
+
}
|
package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js
ADDED
|
@@ -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
|
+
}
|