@dailephd/my-dev-kit-lab 0.3.3 → 0.4.0
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 +46 -269
- package/dist/scripts/security/validate.js +177 -70
- package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +5 -2
- package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +5 -2
- package/dist/src/audits/core/collectSourceFacts.js +21 -10
- package/dist/src/mobile/android/audit/androidFinding.js +68 -0
- package/dist/src/mobile/android/audit/checkResultBuilder.js +97 -0
- package/dist/src/mobile/android/audit/deepLinkAudit.js +183 -0
- package/dist/src/mobile/android/audit/exportedComponentAudit.js +183 -0
- package/dist/src/mobile/android/audit/intentFilterAudit.js +128 -0
- package/dist/src/mobile/android/audit/permissionAudit.js +142 -0
- package/dist/src/mobile/android/detect/androidTargetDetection.js +18 -0
- package/dist/src/mobile/android/detect/buildFileEvidence.js +36 -0
- package/dist/src/mobile/android/detect/classify.js +150 -0
- package/dist/src/mobile/android/detect/detectAndroidProject.js +252 -0
- package/dist/src/mobile/android/detect/settingsModules.js +60 -0
- package/dist/src/mobile/android/detect/sourceEvidence.js +92 -0
- package/dist/src/mobile/android/detect/traversal.js +79 -0
- package/dist/src/mobile/android/detect/versionCatalogEvidence.js +37 -0
- package/dist/src/mobile/android/detection.js +28 -0
- package/dist/src/mobile/android/gradle/gradleMetadataCheckResult.js +120 -0
- package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +137 -0
- package/dist/src/mobile/android/gradle/pluginVersionExtractor.js +40 -0
- package/dist/src/mobile/android/gradle/readAndroidGradleMetadata.js +210 -0
- package/dist/src/mobile/android/gradle/releaseMetadataSummary.js +64 -0
- package/dist/src/mobile/android/gradle/settingsMetadata.js +20 -0
- package/dist/src/mobile/android/gradle/types.js +1 -0
- package/dist/src/mobile/android/gradle/validate/executor.js +9 -0
- package/dist/src/mobile/android/gradle/validate/operationCheckResult.js +99 -0
- package/dist/src/mobile/android/gradle/validate/operations.js +68 -0
- package/dist/src/mobile/android/gradle/validate/planner.js +49 -0
- package/dist/src/mobile/android/gradle/validate/runOptionalGradleValidation.js +113 -0
- package/dist/src/mobile/android/gradle/validate/targetMutation.js +45 -0
- package/dist/src/mobile/android/gradle/validate/taskListParser.js +24 -0
- package/dist/src/mobile/android/gradle/versionCatalogMetadata.js +94 -0
- package/dist/src/mobile/android/gradle/wrapperMetadata.js +32 -0
- package/dist/src/mobile/android/manifest/parseAndroidManifest.js +341 -0
- package/dist/src/mobile/android/manifest/types.js +29 -0
- package/dist/src/mobile/android/manifest/xml/parseXml.js +313 -0
- package/dist/src/mobile/android/profile.js +29 -0
- package/dist/src/mobile/android/report/model.js +104 -0
- package/dist/src/mobile/android/report/renderAndroidReport.js +223 -0
- package/dist/src/mobile/android/report/writeAndroidReportFiles.js +23 -0
- package/dist/src/mobile/android/targetMetadata.js +27 -0
- package/dist/src/mobile/android/validate/androidVerdict.js +248 -0
- package/dist/src/mobile/android/validate/detectionAndImmutabilityCheckResults.js +177 -0
- package/dist/src/mobile/android/validate/playReadinessChecklist.js +145 -0
- package/dist/src/mobile/android/validate/validateAndroidTarget.js +172 -0
- package/dist/src/mobile/android/validation/checkResult.js +51 -0
- package/dist/src/mobile/android/validation/result.js +25 -0
- package/dist/src/mobile/index.js +42 -0
- package/dist/src/mobile/types.js +27 -0
- package/dist/src/securityValidation/attackScenarios/attackProfile.js +2 -0
- package/dist/src/securityValidation/attackScenarios/profiles/androidProfile.js +20 -0
- package/dist/src/securityValidation/validate/cliOptions.js +46 -2
- package/package.json +2 -1
- package/tests/fixtures/android/compose-app/app/build.gradle.kts +21 -0
- package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +22 -0
- package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +11 -0
- package/tests/fixtures/android/compose-app/build.gradle.kts +4 -0
- package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/compose-app/settings.gradle.kts +2 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +3 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +5 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +21 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +17 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +23 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +4 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +3 -0
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +5 -0
- package/tests/fixtures/android/java-app/app/build.gradle +16 -0
- package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +19 -0
- package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +4 -0
- package/tests/fixtures/android/java-app/build.gradle +3 -0
- package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/java-app/settings.gradle +2 -0
- package/tests/fixtures/android/library/build.gradle.kts +3 -0
- package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/library/mylibrary/build.gradle.kts +13 -0
- package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/library/settings.gradle.kts +2 -0
- package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +14 -0
- package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +10 -0
- package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +5 -0
- package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +7 -0
- package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +11 -0
- package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +29 -0
- package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +62 -0
- package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +45 -0
- package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +14 -0
- package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +11 -0
- package/tests/fixtures/android/multi-module/app/build.gradle.kts +22 -0
- package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +3 -0
- package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +11 -0
- package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +3 -0
- package/tests/fixtures/android/multi-module/build.gradle.kts +5 -0
- package/tests/fixtures/android/multi-module/core/build.gradle.kts +14 -0
- package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +18 -0
- package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +2 -0
- package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +8 -0
- package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/multi-module/settings.gradle.kts +4 -0
- package/tests/fixtures/android/non-android-gradle/build.gradle.kts +3 -0
- package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +3 -0
- package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +4 -0
- package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +2 -0
- package/tests/fixtures/android/partial/app/build.gradle.kts +7 -0
- package/tests/fixtures/android/partial/settings.gradle.kts +2 -0
- package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +17 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +31 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +7 -0
- package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +4 -0
- package/tests/fixtures/android/xml-view-app/build.gradle.kts +4 -0
- package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +4 -0
- package/tests/fixtures/android/xml-view-app/settings.gradle.kts +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/README.md +16 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/build.gradle.kts +4 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/java/com/example/Widget.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/kotlin/com/example/Gadget.kt +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/build/Main.class +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/dist/index.js +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/generated/schema.ts +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/vendor/lib.py +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/build.gradle.kts +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/settings.gradle.kts +1 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/java/com/example/FooService.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/kotlin/com/example/BarService.kt +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/java/com/example/FooServiceTest.java +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/kotlin/com/example/BarServiceTest.kt +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/README.md +8 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/app.py +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/tests/test_app.py +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/README.md +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/helper.js +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/tests/index.sample.ts +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/README.md +10 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/build.gradle.kts +4 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/pytests/test_app.py +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/app.py +2 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/helper.js +5 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/index.ts +3 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/java/com/example/Multiplier.java +7 -0
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/kotlin/com/example/Divider.kt +7 -0
|
@@ -42,6 +42,7 @@ export const ARCHITECTURE_DRIFT_DETECTOR = {
|
|
|
42
42
|
},
|
|
43
43
|
run: (ctx) => {
|
|
44
44
|
const issues = [];
|
|
45
|
+
const hasAndroidValidationSource = fs.existsSync(path.join(ctx.target.rootPath, "src", "mobile", "android", "validate", "validateAndroidTarget.ts"));
|
|
45
46
|
const currentStateDocs = ctx.inventory.docsFiles.filter((f) => CURRENT_STATE_DOC_BASENAMES.has(path.basename(f.relativePath).toLowerCase()));
|
|
46
47
|
for (const docFile of currentStateDocs) {
|
|
47
48
|
const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
|
|
@@ -61,7 +62,7 @@ export const ARCHITECTURE_DRIFT_DETECTOR = {
|
|
|
61
62
|
const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
|
|
62
63
|
if (content === null)
|
|
63
64
|
continue;
|
|
64
|
-
issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content));
|
|
65
|
+
issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content, hasAndroidValidationSource));
|
|
65
66
|
}
|
|
66
67
|
return deduplicateIssuesById(issues);
|
|
67
68
|
},
|
|
@@ -227,13 +228,15 @@ function findMajorDirsAbsentFromArchitectureDoc(ctx, docRelativePath, content) {
|
|
|
227
228
|
}
|
|
228
229
|
return issues;
|
|
229
230
|
}
|
|
230
|
-
function findUnsupportedCurrentClaims(docRelativePath, content) {
|
|
231
|
+
function findUnsupportedCurrentClaims(docRelativePath, content, hasAndroidValidationSource) {
|
|
231
232
|
const issues = [];
|
|
232
233
|
const subjects = [
|
|
233
234
|
{ id: "manual-pentest", label: "manual pentest", pattern: "manual pentest" },
|
|
234
235
|
{ id: "android-validation", label: "Android/mobile validation", pattern: "(?:android (?:validation|support))" },
|
|
235
236
|
];
|
|
236
237
|
for (const subject of subjects) {
|
|
238
|
+
if (subject.id === "android-validation" && hasAndroidValidationSource)
|
|
239
|
+
continue;
|
|
237
240
|
const pattern = currentClaimPatternFor(subject.pattern);
|
|
238
241
|
const match = pattern.exec(content);
|
|
239
242
|
if (!match)
|
|
@@ -67,6 +67,7 @@ export const SECURITY_VALIDATION_ASSUMPTION_ROT_DETECTOR = {
|
|
|
67
67
|
const pkgVersion = ctx.sourceOfTruth.package?.version ?? null;
|
|
68
68
|
const hasSecurityValidationSourceDir = ctx.sourceOfTruth.security.hasSecurityValidationSourceDir;
|
|
69
69
|
const hasAttackScenariosDir = fs.existsSync(path.join(ctx.target.rootPath, "src", "securityValidation", "attackScenarios"));
|
|
70
|
+
const hasAndroidValidationSource = fs.existsSync(path.join(ctx.target.rootPath, "src", "mobile", "android", "validate", "validateAndroidTarget.ts"));
|
|
70
71
|
for (const docFile of ctx.inventory.docsFiles) {
|
|
71
72
|
const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
|
|
72
73
|
if (content === null)
|
|
@@ -75,7 +76,7 @@ export const SECURITY_VALIDATION_ASSUMPTION_ROT_DETECTOR = {
|
|
|
75
76
|
issues.push(...findSkippedEqualsPassed(docFile.relativePath, content));
|
|
76
77
|
issues.push(...findExhaustiveSecretClaim(docFile.relativePath, content));
|
|
77
78
|
issues.push(...findCompleteNetworkIsolationClaim(docFile.relativePath, content));
|
|
78
|
-
issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content));
|
|
79
|
+
issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content, hasAndroidValidationSource));
|
|
79
80
|
if (pkgVersion)
|
|
80
81
|
issues.push(...findStaleV021SecurityMention(docFile.relativePath, content, pkgVersion));
|
|
81
82
|
if (hasSecurityValidationSourceDir && hasAttackScenariosDir) {
|
|
@@ -237,13 +238,15 @@ function findCompleteNetworkIsolationClaim(relativePath, content) {
|
|
|
237
238
|
}),
|
|
238
239
|
];
|
|
239
240
|
}
|
|
240
|
-
function findUnsupportedCurrentClaims(relativePath, content) {
|
|
241
|
+
function findUnsupportedCurrentClaims(relativePath, content, hasAndroidValidationSource) {
|
|
241
242
|
const issues = [];
|
|
242
243
|
const subjects = [
|
|
243
244
|
{ id: "manual-pentest", label: "manual pentest", pattern: "manual pentest" },
|
|
244
245
|
{ id: "android-validation", label: "Android/mobile validation", pattern: "(?:android (?:validation|support)|mobile validation)" },
|
|
245
246
|
];
|
|
246
247
|
for (const subject of subjects) {
|
|
248
|
+
if (subject.id === "android-validation" && hasAndroidValidationSource)
|
|
249
|
+
continue;
|
|
247
250
|
const match = currentClaimPatternFor(subject.pattern).exec(content);
|
|
248
251
|
if (!match)
|
|
249
252
|
continue;
|
|
@@ -41,17 +41,18 @@ export async function collectSourceFacts(targetRoot, inventory, registry = DEFAU
|
|
|
41
41
|
// filtering preserves that order, so no re-sort is needed here.
|
|
42
42
|
const eligible = inventory.files.filter((entry) => ANALYZABLE_ROLES.has(entry.role));
|
|
43
43
|
for (const entry of eligible) {
|
|
44
|
+
const relativePath = normalizeRelativePath(entry.relativePath);
|
|
44
45
|
const analyzer = selectLanguageAnalyzer(registry, entry.language, entry.extension);
|
|
45
46
|
if (!analyzer) {
|
|
46
|
-
files.push(buildFallbackFacts(entry));
|
|
47
|
+
files.push(buildFallbackFacts(entry, relativePath));
|
|
47
48
|
continue;
|
|
48
49
|
}
|
|
49
|
-
const absolutePath =
|
|
50
|
+
const absolutePath = resolveInventoryFilePath(resolvedRoot, relativePath);
|
|
50
51
|
try {
|
|
51
52
|
const content = fs.readFileSync(absolutePath, "utf8");
|
|
52
53
|
const facts = await analyzer.analyzeFile({
|
|
53
54
|
targetRoot: resolvedRoot,
|
|
54
|
-
relativePath
|
|
55
|
+
relativePath,
|
|
55
56
|
absolutePath,
|
|
56
57
|
content,
|
|
57
58
|
inventoryEntry: entry,
|
|
@@ -65,13 +66,13 @@ export async function collectSourceFacts(targetRoot, inventory, registry = DEFAU
|
|
|
65
66
|
const diagnostic = {
|
|
66
67
|
severity: "error",
|
|
67
68
|
code: "analyzer-error",
|
|
68
|
-
message: `Analyzer "${analyzer.id}" failed on "${
|
|
69
|
-
path:
|
|
69
|
+
message: `Analyzer "${analyzer.id}" failed on "${relativePath}": ${message}`,
|
|
70
|
+
path: relativePath,
|
|
70
71
|
analyzerId: analyzer.id,
|
|
71
72
|
};
|
|
72
73
|
analyzerDiagnostics.push(diagnostic);
|
|
73
74
|
files.push({
|
|
74
|
-
relativePath
|
|
75
|
+
relativePath,
|
|
75
76
|
language: entry.language,
|
|
76
77
|
role: entry.role,
|
|
77
78
|
parseStatus: "parse-error",
|
|
@@ -94,7 +95,7 @@ export async function collectSourceFacts(targetRoot, inventory, registry = DEFAU
|
|
|
94
95
|
warnings,
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
|
-
function buildFallbackFacts(entry) {
|
|
98
|
+
function buildFallbackFacts(entry, relativePath) {
|
|
98
99
|
const known = KNOWN_LANGUAGES.has(entry.language);
|
|
99
100
|
const parseStatus = known ? "file-level-only" : "unsupported";
|
|
100
101
|
const diagnostic = known
|
|
@@ -102,18 +103,18 @@ function buildFallbackFacts(entry) {
|
|
|
102
103
|
severity: "info",
|
|
103
104
|
code: "no-analyzer-registered",
|
|
104
105
|
message: `No language analyzer is registered for "${entry.language}" yet; only inventory-level facts are available.`,
|
|
105
|
-
path:
|
|
106
|
+
path: relativePath,
|
|
106
107
|
analyzerId: null,
|
|
107
108
|
}
|
|
108
109
|
: {
|
|
109
110
|
severity: "info",
|
|
110
111
|
code: "unsupported-language",
|
|
111
112
|
message: `Language "${entry.language}" is not supported for source-fact extraction.`,
|
|
112
|
-
path:
|
|
113
|
+
path: relativePath,
|
|
113
114
|
analyzerId: null,
|
|
114
115
|
};
|
|
115
116
|
return {
|
|
116
|
-
relativePath
|
|
117
|
+
relativePath,
|
|
117
118
|
language: entry.language,
|
|
118
119
|
role: entry.role,
|
|
119
120
|
parseStatus,
|
|
@@ -126,6 +127,16 @@ function buildFallbackFacts(entry) {
|
|
|
126
127
|
diagnostics: [diagnostic],
|
|
127
128
|
};
|
|
128
129
|
}
|
|
130
|
+
function normalizeRelativePath(relativePath) {
|
|
131
|
+
const normalized = path.posix.normalize(relativePath.replace(/\\/g, "/"));
|
|
132
|
+
if (normalized === ".")
|
|
133
|
+
return "";
|
|
134
|
+
return normalized.replace(/^\/+/, "").replace(/^\.\//, "");
|
|
135
|
+
}
|
|
136
|
+
function resolveInventoryFilePath(root, relativePath) {
|
|
137
|
+
const parts = relativePath.split("/").filter((part) => part.length > 0);
|
|
138
|
+
return parts.length === 0 ? root : path.resolve(root, ...parts);
|
|
139
|
+
}
|
|
129
140
|
function countByLanguage(files) {
|
|
130
141
|
const counts = Object.fromEntries(NORMALIZED_LANGUAGES.map((l) => [l, 0]));
|
|
131
142
|
for (const file of files) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.0 Batch 3 — Android SecurityFinding factory.
|
|
3
|
+
//
|
|
4
|
+
// Reuses SecurityFinding as-is (no parallel Android finding schema). Android
|
|
5
|
+
// findings use category "static-scan" — an existing SecurityCheckCategory
|
|
6
|
+
// value that already covers static-analysis-derived findings (CodeQL,
|
|
7
|
+
// Semgrep) and accurately describes manifest analysis, so no widening of the
|
|
8
|
+
// shared, heavily-tested SecurityCheckCategory union was needed. Per-finding
|
|
9
|
+
// audit family is instead carried by the owning AndroidCheckResult.category
|
|
10
|
+
// (android-permissions/android-components/android-intent-filters/
|
|
11
|
+
// android-deep-links) and by the finding id/title.
|
|
12
|
+
//
|
|
13
|
+
// SecurityFinding has no `confidence` or structured `location` field (the
|
|
14
|
+
// existing codebase convention, e.g. AttackResult, keeps confidence on the
|
|
15
|
+
// *check result*, not the individual finding — see
|
|
16
|
+
// src/securityValidation/attackScenarios/attackResult.ts). Android check
|
|
17
|
+
// results already carry `confidence: MobileConfidence` (Batch 1). Per-finding
|
|
18
|
+
// confidence and location are therefore folded into the free-text `evidence`
|
|
19
|
+
// field here, consistent with how existing scenario code joins evidence
|
|
20
|
+
// previews into that same field.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
function slug(value) {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
25
|
+
.replace(/^-+|-+$/g, "")
|
|
26
|
+
.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
// Deterministic, content-derived id — never depends on a counter, timestamp,
|
|
29
|
+
// or enumeration order, so equivalent evidence always yields the same id.
|
|
30
|
+
function buildFindingId(ruleId, manifestPath, identity, location) {
|
|
31
|
+
const parts = [ruleId, slug(manifestPath), identity ? slug(identity) : undefined, location?.line !== undefined ? `l${location.line}` : undefined].filter((part) => Boolean(part));
|
|
32
|
+
return parts.join("--");
|
|
33
|
+
}
|
|
34
|
+
function defaultReleaseImpact(severity) {
|
|
35
|
+
switch (severity) {
|
|
36
|
+
case "blocker":
|
|
37
|
+
return "Must fix before release";
|
|
38
|
+
case "major":
|
|
39
|
+
return "Should fix before release";
|
|
40
|
+
case "minor":
|
|
41
|
+
return "Review recommended before release";
|
|
42
|
+
case "informational":
|
|
43
|
+
return "No release impact; informational only";
|
|
44
|
+
case "skipped":
|
|
45
|
+
return "Not evaluated";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function makeAndroidFinding(input) {
|
|
49
|
+
const id = buildFindingId(input.ruleId, input.manifestPath, input.identity, input.location);
|
|
50
|
+
const locationSuffix = input.location?.line !== undefined ? `:${input.location.line}` : "";
|
|
51
|
+
const evidenceParts = [
|
|
52
|
+
`confidence=${input.confidence}`,
|
|
53
|
+
input.identity ? `identity=${input.identity}` : undefined,
|
|
54
|
+
`location=${input.manifestPath}${locationSuffix}`,
|
|
55
|
+
...(input.evidenceDetails ?? []),
|
|
56
|
+
].filter((part) => Boolean(part));
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
title: input.title,
|
|
60
|
+
severity: input.severity,
|
|
61
|
+
category: "static-scan",
|
|
62
|
+
description: input.description,
|
|
63
|
+
evidence: evidenceParts.join(" | "),
|
|
64
|
+
affectedFiles: [input.manifestPath],
|
|
65
|
+
recommendation: input.recommendation,
|
|
66
|
+
releaseImpact: input.releaseImpact ?? defaultReleaseImpact(input.severity),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
function isMalformed(entry) {
|
|
2
|
+
return entry.manifest.parseWarnings.some((w) => w.startsWith("Malformed XML:"));
|
|
3
|
+
}
|
|
4
|
+
export function buildAndroidManifestCheckResult(input) {
|
|
5
|
+
const { id, category, title, detection, manifests, findings, evidence, warnings } = input;
|
|
6
|
+
const sourcePaths = manifests.map((m) => m.manifestPath);
|
|
7
|
+
if (detection.projectKind === "non-android") {
|
|
8
|
+
return {
|
|
9
|
+
id,
|
|
10
|
+
category,
|
|
11
|
+
title,
|
|
12
|
+
status: "unsupported",
|
|
13
|
+
requirementLevel: "optional",
|
|
14
|
+
ran: false,
|
|
15
|
+
skipped: true,
|
|
16
|
+
skipInfo: {
|
|
17
|
+
checkId: id,
|
|
18
|
+
reason: "Target was not detected as an Android project.",
|
|
19
|
+
requirementLevel: "optional",
|
|
20
|
+
missingCapability: "android-detection",
|
|
21
|
+
verdictImpact: "does not apply to a non-Android target",
|
|
22
|
+
recommendedNextAction: "Re-run against an Android Gradle project, or verify project detection.",
|
|
23
|
+
},
|
|
24
|
+
evidence: [],
|
|
25
|
+
findings: [],
|
|
26
|
+
warnings: [],
|
|
27
|
+
errors: [],
|
|
28
|
+
sourcePaths: [],
|
|
29
|
+
confidence: "unknown",
|
|
30
|
+
environmentRequirements: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (manifests.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
category,
|
|
37
|
+
title,
|
|
38
|
+
status: "inconclusive",
|
|
39
|
+
requirementLevel: "required",
|
|
40
|
+
ran: true,
|
|
41
|
+
skipped: false,
|
|
42
|
+
evidence,
|
|
43
|
+
findings: [],
|
|
44
|
+
warnings: [...warnings, "No AndroidManifest.xml was found for this target; audit could not run."],
|
|
45
|
+
errors: [],
|
|
46
|
+
sourcePaths: [],
|
|
47
|
+
confidence: "unknown",
|
|
48
|
+
environmentRequirements: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const malformedEntries = manifests.filter(isMalformed);
|
|
52
|
+
const allMalformed = malformedEntries.length === manifests.length;
|
|
53
|
+
if (allMalformed) {
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
category,
|
|
57
|
+
title,
|
|
58
|
+
status: "error",
|
|
59
|
+
requirementLevel: "required",
|
|
60
|
+
ran: true,
|
|
61
|
+
skipped: false,
|
|
62
|
+
evidence,
|
|
63
|
+
findings: [],
|
|
64
|
+
warnings,
|
|
65
|
+
errors: malformedEntries.flatMap((entry) => entry.manifest.parseWarnings.filter((w) => w.startsWith("Malformed XML:"))),
|
|
66
|
+
sourcePaths,
|
|
67
|
+
confidence: "unknown",
|
|
68
|
+
environmentRequirements: [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const confidence = malformedEntries.length > 0 ? "medium" : "high";
|
|
72
|
+
// Informational-only findings (e.g. "standard launcher activity is
|
|
73
|
+
// exported, as expected") are evidence/inventory, not a reportable issue —
|
|
74
|
+
// only minor/major/blocker findings cause a "failed" status, matching
|
|
75
|
+
// section 7.17's "completed and found no reportable issues may pass".
|
|
76
|
+
const hasReportableFinding = findings.some((finding) => finding.severity !== "informational");
|
|
77
|
+
const status = hasReportableFinding ? "failed" : "passed";
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
category,
|
|
81
|
+
title,
|
|
82
|
+
status,
|
|
83
|
+
requirementLevel: "required",
|
|
84
|
+
ran: true,
|
|
85
|
+
skipped: false,
|
|
86
|
+
evidence,
|
|
87
|
+
findings,
|
|
88
|
+
warnings: [
|
|
89
|
+
...warnings,
|
|
90
|
+
...malformedEntries.flatMap((entry) => entry.manifest.parseWarnings.filter((w) => w.startsWith("Malformed XML:"))),
|
|
91
|
+
],
|
|
92
|
+
errors: [],
|
|
93
|
+
sourcePaths,
|
|
94
|
+
confidence,
|
|
95
|
+
environmentRequirements: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { makeAndroidFinding } from "./androidFinding.js";
|
|
2
|
+
import { buildAndroidManifestCheckResult } from "./checkResultBuilder.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.0 Batch 3 — initial deep-link audit.
|
|
5
|
+
//
|
|
6
|
+
// Distinguishes web links (http/https) from custom-scheme deep links, flags
|
|
7
|
+
// missing host/scheme restrictions and broad path patterns, and treats
|
|
8
|
+
// BROWSABLE-without-VIEW and VIEW-without-BROWSABLE as separate "incomplete
|
|
9
|
+
// filter" observations (agents.txt Batch 3 section 7.16). Never claims
|
|
10
|
+
// Digital Asset Links verification, domain ownership, or exploitability —
|
|
11
|
+
// findings are framed as static exposure/configuration review, at most
|
|
12
|
+
// "major" for the narrow high-confidence case of an explicitly exported,
|
|
13
|
+
// unprotected, wildcard-path activity.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const VIEW_ACTION = "android.intent.action.VIEW";
|
|
16
|
+
const BROWSABLE_CATEGORY = "android.intent.category.BROWSABLE";
|
|
17
|
+
const WILDCARD_PATTERN = /[*.]{1,2}\*|\.\*/;
|
|
18
|
+
function isPlaceholder(value) {
|
|
19
|
+
return value !== undefined && (value.includes("${") || value.startsWith("@"));
|
|
20
|
+
}
|
|
21
|
+
function classifyScheme(dataElements) {
|
|
22
|
+
const schemes = dataElements.map((d) => d.dataScheme).filter((s) => Boolean(s));
|
|
23
|
+
if (schemes.length === 0)
|
|
24
|
+
return "none";
|
|
25
|
+
if (schemes.some((s) => s === "http" || s === "https"))
|
|
26
|
+
return "web";
|
|
27
|
+
return "custom";
|
|
28
|
+
}
|
|
29
|
+
function auditDeepLinkFilter(entry, kind, component, filter, findings) {
|
|
30
|
+
const identity = component.name || "(unnamed component)";
|
|
31
|
+
const actions = filter.actions ?? [];
|
|
32
|
+
const categories = filter.categories ?? [];
|
|
33
|
+
const dataElements = filter.dataElements ?? filter.filterData;
|
|
34
|
+
const hasView = actions.includes(VIEW_ACTION);
|
|
35
|
+
const hasBrowsable = categories.includes(BROWSABLE_CATEGORY);
|
|
36
|
+
if (!hasView && !hasBrowsable)
|
|
37
|
+
return;
|
|
38
|
+
if (hasBrowsable && !hasView) {
|
|
39
|
+
findings.push(makeAndroidFinding({
|
|
40
|
+
ruleId: "android-deep-link-browsable-without-view",
|
|
41
|
+
title: `BROWSABLE category without a VIEW action on ${kind} "${identity}"`,
|
|
42
|
+
severity: "informational",
|
|
43
|
+
confidence: "high",
|
|
44
|
+
description: "This <intent-filter> declares category BROWSABLE without action VIEW, so it will not actually be treated as a browsable web/deep link by the platform.",
|
|
45
|
+
manifestPath: entry.manifestPath,
|
|
46
|
+
identity,
|
|
47
|
+
location: filter.location,
|
|
48
|
+
}));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (hasView && !hasBrowsable && dataElements.length === 0) {
|
|
52
|
+
// Plain VIEW with no data and no BROWSABLE — not a deep link, nothing to audit here.
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const exportedExplicitTrue = component.exported === true;
|
|
56
|
+
const unprotected = !component.permission;
|
|
57
|
+
const scheme = classifyScheme(dataElements);
|
|
58
|
+
const hasHost = dataElements.some((d) => Boolean(d.dataHost));
|
|
59
|
+
const placeholderEvidence = dataElements.some((d) => isPlaceholder(d.dataScheme) || isPlaceholder(d.dataHost) || isPlaceholder(d.dataPath) || isPlaceholder(d.dataPathPrefix) || isPlaceholder(d.dataPathPattern));
|
|
60
|
+
const broadPattern = dataElements.some((d) => (d.dataPathPattern && WILDCARD_PATTERN.test(d.dataPathPattern)) || d.dataPathPattern === ".*" || d.dataPathPattern === "*");
|
|
61
|
+
const evidenceDetails = [
|
|
62
|
+
`scheme=${scheme}`,
|
|
63
|
+
`hasHost=${hasHost}`,
|
|
64
|
+
`exported=${component.exported ?? "unspecified"}`,
|
|
65
|
+
`hasComponentPermission=${!unprotected}`,
|
|
66
|
+
`dataElementCount=${dataElements.length}`,
|
|
67
|
+
];
|
|
68
|
+
if (placeholderEvidence) {
|
|
69
|
+
findings.push(makeAndroidFinding({
|
|
70
|
+
ruleId: "android-deep-link-placeholder-value",
|
|
71
|
+
title: `Deep-link data on ${kind} "${identity}" contains an unresolved placeholder or resource reference`,
|
|
72
|
+
severity: "informational",
|
|
73
|
+
confidence: "low",
|
|
74
|
+
description: "One or more deep-link data attributes (scheme/host/path) reference an unresolved manifest placeholder (e.g. ${applicationId}) or Android resource (e.g. @string/...). Effective exposure cannot be assessed statically.",
|
|
75
|
+
manifestPath: entry.manifestPath,
|
|
76
|
+
identity,
|
|
77
|
+
location: filter.location,
|
|
78
|
+
evidenceDetails,
|
|
79
|
+
}));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (broadPattern) {
|
|
83
|
+
const strongEvidence = exportedExplicitTrue && unprotected;
|
|
84
|
+
findings.push(makeAndroidFinding({
|
|
85
|
+
ruleId: "android-deep-link-broad-path-pattern",
|
|
86
|
+
title: `Broad/wildcard deep-link path pattern on ${kind} "${identity}"`,
|
|
87
|
+
severity: strongEvidence ? "major" : "minor",
|
|
88
|
+
confidence: strongEvidence ? "high" : "medium",
|
|
89
|
+
description: strongEvidence
|
|
90
|
+
? "This deep-link filter uses a highly permissive pathPattern, the component is explicitly exported, and no component-level permission protects it — a strong static combination of broad matching and open reachability."
|
|
91
|
+
: "This deep-link filter uses a broad/wildcard-like pathPattern. Broad path matching is not automatically exploitable, but widens the set of URLs the app will handle.",
|
|
92
|
+
manifestPath: entry.manifestPath,
|
|
93
|
+
identity,
|
|
94
|
+
location: filter.location,
|
|
95
|
+
evidenceDetails,
|
|
96
|
+
recommendation: "Narrow the pathPattern to only the routes the app actually needs to handle, and validate any path/query data before use.",
|
|
97
|
+
}));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (scheme === "web" && !hasHost) {
|
|
101
|
+
findings.push(makeAndroidFinding({
|
|
102
|
+
ruleId: "android-deep-link-web-link-without-host",
|
|
103
|
+
title: `HTTP/HTTPS deep link on ${kind} "${identity}" has no host restriction`,
|
|
104
|
+
severity: "minor",
|
|
105
|
+
confidence: "medium",
|
|
106
|
+
description: "This web-link intent filter declares an http/https scheme without a host, so it is not restricted to a specific domain.",
|
|
107
|
+
manifestPath: entry.manifestPath,
|
|
108
|
+
identity,
|
|
109
|
+
location: filter.location,
|
|
110
|
+
evidenceDetails,
|
|
111
|
+
recommendation: "Add android:host to restrict which domains this filter matches.",
|
|
112
|
+
}));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (scheme === "custom" && !hasHost) {
|
|
116
|
+
findings.push(makeAndroidFinding({
|
|
117
|
+
ruleId: "android-deep-link-custom-scheme-without-host",
|
|
118
|
+
title: `Custom-scheme deep link on ${kind} "${identity}" has no host restriction`,
|
|
119
|
+
severity: "minor",
|
|
120
|
+
confidence: "medium",
|
|
121
|
+
description: "This custom-scheme intent filter declares no host restriction. Custom schemes are not automatically vulnerabilities, but an unrestricted scheme accepts any host/path combination.",
|
|
122
|
+
manifestPath: entry.manifestPath,
|
|
123
|
+
identity,
|
|
124
|
+
location: filter.location,
|
|
125
|
+
evidenceDetails,
|
|
126
|
+
recommendation: "Consider restricting the host or validating incoming URIs defensively in the handling code.",
|
|
127
|
+
}));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Bounded/safe deep link: explicit scheme + host (+ optional path
|
|
131
|
+
// restriction), no wildcard pattern, no placeholders — recorded for
|
|
132
|
+
// inventory purposes at informational severity only (ANDROID-B3-24).
|
|
133
|
+
findings.push(makeAndroidFinding({
|
|
134
|
+
ruleId: "android-deep-link-bounded",
|
|
135
|
+
title: `Deep link declared on ${kind} "${identity}"`,
|
|
136
|
+
severity: "informational",
|
|
137
|
+
confidence: "high",
|
|
138
|
+
description: "This deep-link filter declares an explicit scheme and host with bounded path evidence. No static exposure concern was identified.",
|
|
139
|
+
manifestPath: entry.manifestPath,
|
|
140
|
+
identity,
|
|
141
|
+
location: filter.location,
|
|
142
|
+
evidenceDetails,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
function componentGroups(entry) {
|
|
146
|
+
return [
|
|
147
|
+
{ kind: "activity", components: entry.manifest.activities },
|
|
148
|
+
{ kind: "activity-alias", components: entry.manifest.activityAliases ?? [] },
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
export function auditAndroidDeepLinks(detection, manifests) {
|
|
152
|
+
const findings = [];
|
|
153
|
+
const evidence = [];
|
|
154
|
+
const warnings = [];
|
|
155
|
+
for (const entry of manifests) {
|
|
156
|
+
let candidateCount = 0;
|
|
157
|
+
for (const group of componentGroups(entry)) {
|
|
158
|
+
for (const component of group.components) {
|
|
159
|
+
for (const filter of component.intentFilters) {
|
|
160
|
+
const actions = filter.actions ?? [];
|
|
161
|
+
const categories = filter.categories ?? [];
|
|
162
|
+
if (actions.includes(VIEW_ACTION) || categories.includes(BROWSABLE_CATEGORY)) {
|
|
163
|
+
candidateCount += 1;
|
|
164
|
+
auditDeepLinkFilter(entry, group.kind, component, filter, findings);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (candidateCount > 0) {
|
|
170
|
+
evidence.push(`${entry.manifestPath}: ${candidateCount} deep-link candidate intent-filter(s) inspected`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return buildAndroidManifestCheckResult({
|
|
174
|
+
id: "android-deep-links-audit",
|
|
175
|
+
category: "android-deep-links",
|
|
176
|
+
title: "Android deep-link audit",
|
|
177
|
+
detection,
|
|
178
|
+
manifests,
|
|
179
|
+
findings,
|
|
180
|
+
evidence,
|
|
181
|
+
warnings,
|
|
182
|
+
});
|
|
183
|
+
}
|