@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { makeAndroidFinding } from "./androidFinding.js";
|
|
2
|
+
import { buildAndroidManifestCheckResult } from "./checkResultBuilder.js";
|
|
3
|
+
function componentGroups(manifest) {
|
|
4
|
+
return [
|
|
5
|
+
{ kind: "activity", components: manifest.activities },
|
|
6
|
+
{ kind: "activity-alias", components: manifest.activityAliases ?? [] },
|
|
7
|
+
{ kind: "service", components: manifest.services },
|
|
8
|
+
{ kind: "receiver", components: manifest.receivers },
|
|
9
|
+
{ kind: "provider", components: manifest.providers },
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
function isProtectedNonProvider(component, applicationPermission) {
|
|
13
|
+
return Boolean(component.permission) || Boolean(applicationPermission);
|
|
14
|
+
}
|
|
15
|
+
function providerProtection(component) {
|
|
16
|
+
if (component.permission)
|
|
17
|
+
return "protected";
|
|
18
|
+
if (component.readPermission && component.writePermission)
|
|
19
|
+
return "protected";
|
|
20
|
+
if (component.readPermission || component.writePermission)
|
|
21
|
+
return "partial";
|
|
22
|
+
return "unprotected";
|
|
23
|
+
}
|
|
24
|
+
export function auditAndroidExportedComponents(detection, manifests) {
|
|
25
|
+
const findings = [];
|
|
26
|
+
const evidence = [];
|
|
27
|
+
const warnings = [];
|
|
28
|
+
for (const entry of manifests) {
|
|
29
|
+
const applicationPermission = entry.manifest.application.permission;
|
|
30
|
+
let inspectedCount = 0;
|
|
31
|
+
for (const group of componentGroups(entry.manifest)) {
|
|
32
|
+
for (const component of group.components) {
|
|
33
|
+
inspectedCount += 1;
|
|
34
|
+
const hasFilters = component.intentFilters.length > 0;
|
|
35
|
+
const identity = component.name || "(unnamed component)";
|
|
36
|
+
// Malformed exported value: never normalized to true or false.
|
|
37
|
+
if (component.exported === undefined && component.exportedRaw !== undefined) {
|
|
38
|
+
findings.push(makeAndroidFinding({
|
|
39
|
+
ruleId: "android-component-malformed-exported",
|
|
40
|
+
title: `${group.kind} "${identity}" has a malformed android:exported value`,
|
|
41
|
+
severity: "minor",
|
|
42
|
+
confidence: "low",
|
|
43
|
+
description: `android:exported="${component.exportedRaw}" could not be statically resolved to true or false. Effective exported state is unknown and must be reviewed manually.`,
|
|
44
|
+
manifestPath: entry.manifestPath,
|
|
45
|
+
identity,
|
|
46
|
+
location: component.location,
|
|
47
|
+
evidenceDetails: [`kind=${group.kind}`, `exportedRaw=${component.exportedRaw}`],
|
|
48
|
+
recommendation: "Resolve the android:exported value to an explicit true/false literal.",
|
|
49
|
+
}));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (component.exported === false) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (component.exported === undefined) {
|
|
56
|
+
if (hasFilters) {
|
|
57
|
+
findings.push(makeAndroidFinding({
|
|
58
|
+
ruleId: "android-component-unspecified-exported-with-filter",
|
|
59
|
+
title: `${group.kind} "${identity}" has an intent filter but no explicit android:exported value`,
|
|
60
|
+
severity: "minor",
|
|
61
|
+
confidence: "medium",
|
|
62
|
+
description: "This component declares an intent filter but does not set android:exported explicitly. Effective exported state depends on the target/compile SDK and manifest context, and must be reviewed rather than assumed.",
|
|
63
|
+
manifestPath: entry.manifestPath,
|
|
64
|
+
identity,
|
|
65
|
+
location: component.location,
|
|
66
|
+
evidenceDetails: [`kind=${group.kind}`, `intentFilterCount=${component.intentFilters.length}`],
|
|
67
|
+
recommendation: "Set android:exported explicitly to document the intended behavior.",
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// component.exported === true from here on.
|
|
73
|
+
if (group.kind === "provider") {
|
|
74
|
+
const protection = providerProtection(component);
|
|
75
|
+
if (protection === "unprotected") {
|
|
76
|
+
findings.push(makeAndroidFinding({
|
|
77
|
+
ruleId: "android-provider-exported-unprotected",
|
|
78
|
+
title: `Exported provider "${identity}" has no permission protection`,
|
|
79
|
+
severity: "major",
|
|
80
|
+
confidence: "high",
|
|
81
|
+
description: "This <provider> is explicitly exported and declares no permission, readPermission, or writePermission. Any app on the device may query or modify its data unless finer-grained protection exists elsewhere (e.g. path-level permissions not visible here).",
|
|
82
|
+
manifestPath: entry.manifestPath,
|
|
83
|
+
identity,
|
|
84
|
+
location: component.location,
|
|
85
|
+
evidenceDetails: [
|
|
86
|
+
`authorities=${(component.authorities ?? []).join(",") || "(none parsed)"}`,
|
|
87
|
+
`grantUriPermissions=${component.grantUriPermissions ?? "unspecified"}`,
|
|
88
|
+
],
|
|
89
|
+
recommendation: "Add android:permission, or both readPermission and writePermission, or set android:exported=\"false\" if external access is not required.",
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
else if (protection === "partial") {
|
|
93
|
+
findings.push(makeAndroidFinding({
|
|
94
|
+
ruleId: "android-provider-exported-partial-protection",
|
|
95
|
+
title: `Exported provider "${identity}" only protects one of read/write access`,
|
|
96
|
+
severity: "minor",
|
|
97
|
+
confidence: "medium",
|
|
98
|
+
description: "This <provider> defines only one of readPermission/writePermission, leaving the other operation unprotected.",
|
|
99
|
+
manifestPath: entry.manifestPath,
|
|
100
|
+
identity,
|
|
101
|
+
location: component.location,
|
|
102
|
+
evidenceDetails: [`readPermission=${component.readPermission ?? "(none)"}`, `writePermission=${component.writePermission ?? "(none)"}`],
|
|
103
|
+
recommendation: "Protect both read and write access, or document why only one is intentionally protected.",
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
findings.push(makeAndroidFinding({
|
|
108
|
+
ruleId: "android-provider-exported-protected",
|
|
109
|
+
title: `Exported provider "${identity}" is permission-protected`,
|
|
110
|
+
severity: "informational",
|
|
111
|
+
confidence: "high",
|
|
112
|
+
description: "This <provider> is explicitly exported but protection evidence (permission or read+write permissions) is present.",
|
|
113
|
+
manifestPath: entry.manifestPath,
|
|
114
|
+
identity,
|
|
115
|
+
location: component.location,
|
|
116
|
+
evidenceDetails: [`permission=${component.permission ?? "(none)"}`],
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const protected_ = isProtectedNonProvider(component, applicationPermission);
|
|
122
|
+
if (protected_) {
|
|
123
|
+
findings.push(makeAndroidFinding({
|
|
124
|
+
ruleId: "android-component-exported-protected",
|
|
125
|
+
title: `Exported ${group.kind} "${identity}" is permission-protected`,
|
|
126
|
+
severity: "informational",
|
|
127
|
+
confidence: "high",
|
|
128
|
+
description: `This ${group.kind} is explicitly exported and protected by ${component.permission ? `component-level permission "${component.permission}"` : `an application-level permission ("${applicationPermission}")`}.`,
|
|
129
|
+
manifestPath: entry.manifestPath,
|
|
130
|
+
identity,
|
|
131
|
+
location: component.location,
|
|
132
|
+
evidenceDetails: [`kind=${group.kind}`, `hasIntentFilters=${hasFilters}`, `permission=${component.permission ?? applicationPermission}`],
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
else if (component.isLauncherActivity) {
|
|
136
|
+
// A standard launcher activity being exported and unprotected is
|
|
137
|
+
// expected, required Android behavior (it must be exported to be
|
|
138
|
+
// launchable from the home screen) — informational only, not a
|
|
139
|
+
// review-required finding (agents.txt Batch 3 section 7.19,
|
|
140
|
+
// "Standard launcher activity evidence" = informational/low tier).
|
|
141
|
+
findings.push(makeAndroidFinding({
|
|
142
|
+
ruleId: "android-component-launcher-activity-exported",
|
|
143
|
+
title: `Launcher activity "${identity}" is exported (expected)`,
|
|
144
|
+
severity: "informational",
|
|
145
|
+
confidence: "high",
|
|
146
|
+
description: "This is the app's launcher activity (MAIN/LAUNCHER intent filter). Being exported without permission protection is expected, standard behavior for a launcher entry point.",
|
|
147
|
+
manifestPath: entry.manifestPath,
|
|
148
|
+
identity,
|
|
149
|
+
location: component.location,
|
|
150
|
+
evidenceDetails: [`kind=${group.kind}`],
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
findings.push(makeAndroidFinding({
|
|
155
|
+
ruleId: "android-component-exported-unprotected",
|
|
156
|
+
title: `Exported ${group.kind} "${identity}" has no deterministic permission protection`,
|
|
157
|
+
severity: "minor",
|
|
158
|
+
confidence: hasFilters ? "high" : "medium",
|
|
159
|
+
description: `This ${group.kind} is explicitly exported and no component-level or application-level permission was found in this manifest. Effective protection could still exist via a permission defined in another source set or module, which lowers confidence here.`,
|
|
160
|
+
manifestPath: entry.manifestPath,
|
|
161
|
+
identity,
|
|
162
|
+
location: component.location,
|
|
163
|
+
evidenceDetails: [`kind=${group.kind}`, `hasIntentFilters=${hasFilters}`],
|
|
164
|
+
recommendation: "Add android:permission if this component should not be freely reachable by other apps, or confirm android:exported=\"true\" is intentional.",
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (inspectedCount > 0) {
|
|
170
|
+
evidence.push(`${entry.manifestPath}: ${inspectedCount} component(s) inspected`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return buildAndroidManifestCheckResult({
|
|
174
|
+
id: "android-exported-components-audit",
|
|
175
|
+
category: "android-components",
|
|
176
|
+
title: "Android exported-component audit",
|
|
177
|
+
detection,
|
|
178
|
+
manifests,
|
|
179
|
+
findings,
|
|
180
|
+
evidence,
|
|
181
|
+
warnings,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { makeAndroidFinding } from "./androidFinding.js";
|
|
2
|
+
import { buildAndroidManifestCheckResult } from "./checkResultBuilder.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.0 Batch 3 — initial intent-filter audit.
|
|
5
|
+
//
|
|
6
|
+
// Distinct lens from the exported-component audit (agents.txt Batch 3
|
|
7
|
+
// section 7.15): this audit does not repeat "is this exported and
|
|
8
|
+
// unprotected" — it looks at the intent-filter *contents* themselves:
|
|
9
|
+
// duplicate/malformed filter data, and custom (non-platform) actions on a
|
|
10
|
+
// component that is reachable (explicitly exported, or unspecified-exported
|
|
11
|
+
// with a filter). A standard MAIN/LAUNCHER filter is never flagged — that is
|
|
12
|
+
// the required negative control (ANDROID-B3-22).
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const STANDARD_ACTION_PREFIX = "android.intent.action.";
|
|
15
|
+
const STANDARD_CATEGORY_PREFIX = "android.intent.category.";
|
|
16
|
+
function isReachable(component) {
|
|
17
|
+
return component.exported === true || (component.exported === undefined && component.exportedRaw === undefined && component.intentFilters.length > 0);
|
|
18
|
+
}
|
|
19
|
+
function hasDuplicates(values) {
|
|
20
|
+
return new Set(values).size !== values.length;
|
|
21
|
+
}
|
|
22
|
+
function auditFilter(entry, kind, component, filter, findings) {
|
|
23
|
+
const identity = component.name || "(unnamed component)";
|
|
24
|
+
const actions = filter.actions ?? [];
|
|
25
|
+
const categories = filter.categories ?? [];
|
|
26
|
+
const dataElements = filter.dataElements ?? filter.filterData;
|
|
27
|
+
if (actions.length === 0 && dataElements.length === 0) {
|
|
28
|
+
findings.push(makeAndroidFinding({
|
|
29
|
+
ruleId: "android-intent-filter-empty-or-malformed",
|
|
30
|
+
title: `Empty or malformed <intent-filter> on ${kind} "${identity}"`,
|
|
31
|
+
severity: "informational",
|
|
32
|
+
confidence: "medium",
|
|
33
|
+
description: "This <intent-filter> declares no actions and no data elements, so it cannot match any implicit intent.",
|
|
34
|
+
manifestPath: entry.manifestPath,
|
|
35
|
+
identity,
|
|
36
|
+
location: filter.location,
|
|
37
|
+
evidenceDetails: [`kind=${kind}`],
|
|
38
|
+
}));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (hasDuplicates(actions) || hasDuplicates(categories)) {
|
|
42
|
+
findings.push(makeAndroidFinding({
|
|
43
|
+
ruleId: "android-intent-filter-duplicate-entries",
|
|
44
|
+
title: `Duplicate action/category entries in an <intent-filter> on ${kind} "${identity}"`,
|
|
45
|
+
severity: "informational",
|
|
46
|
+
confidence: "high",
|
|
47
|
+
description: "This <intent-filter> declares the same <action> or <category> more than once, which has no additional effect and may indicate a copy/paste mistake.",
|
|
48
|
+
manifestPath: entry.manifestPath,
|
|
49
|
+
identity,
|
|
50
|
+
location: filter.location,
|
|
51
|
+
evidenceDetails: [`actions=${actions.join(",")}`, `categories=${categories.join(",")}`],
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
const isStandardLauncherFilter = actions.length === 1 &&
|
|
55
|
+
actions[0] === "android.intent.action.MAIN" &&
|
|
56
|
+
categories.every((c) => c === "android.intent.category.LAUNCHER" || c === "android.intent.category.DEFAULT");
|
|
57
|
+
if (isStandardLauncherFilter) {
|
|
58
|
+
// Negative control: a standard launcher entry point is expected and
|
|
59
|
+
// must not itself be reported as a finding (ANDROID-B3-22).
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const customActions = actions.filter((action) => !action.startsWith(STANDARD_ACTION_PREFIX));
|
|
63
|
+
if (customActions.length > 0 && isReachable(component)) {
|
|
64
|
+
findings.push(makeAndroidFinding({
|
|
65
|
+
ruleId: "android-intent-filter-custom-action-reachable",
|
|
66
|
+
title: `Custom action on a potentially reachable ${kind} "${identity}"`,
|
|
67
|
+
severity: "minor",
|
|
68
|
+
confidence: component.exported === true ? "high" : "medium",
|
|
69
|
+
description: `This <intent-filter> declares a custom (non-platform) action (${customActions.join(", ")}) on a component whose exported state is ${component.exported === true ? "explicitly true" : "unspecified with an intent filter present"}. Custom actions on reachable components broaden the app's externally triggerable surface and are worth reviewing, though the action name alone does not prove exploitability.`,
|
|
70
|
+
manifestPath: entry.manifestPath,
|
|
71
|
+
identity,
|
|
72
|
+
location: filter.location,
|
|
73
|
+
evidenceDetails: [`customActions=${customActions.join(",")}`, `exported=${component.exported ?? "unspecified"}`, `hasComponentPermission=${Boolean(component.permission)}`],
|
|
74
|
+
recommendation: "Confirm this custom action needs to be externally reachable, and add permission protection if not.",
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
const unrecognizedCategories = categories.filter((c) => !c.startsWith(STANDARD_CATEGORY_PREFIX));
|
|
78
|
+
if (unrecognizedCategories.length > 0) {
|
|
79
|
+
findings.push(makeAndroidFinding({
|
|
80
|
+
ruleId: "android-intent-filter-non-standard-category",
|
|
81
|
+
title: `Non-standard intent-filter category on ${kind} "${identity}"`,
|
|
82
|
+
severity: "informational",
|
|
83
|
+
confidence: "medium",
|
|
84
|
+
description: `This <intent-filter> declares a category outside the standard android.intent.category.* namespace (${unrecognizedCategories.join(", ")}).`,
|
|
85
|
+
manifestPath: entry.manifestPath,
|
|
86
|
+
identity,
|
|
87
|
+
location: filter.location,
|
|
88
|
+
evidenceDetails: [`categories=${unrecognizedCategories.join(",")}`],
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function componentGroups(manifest) {
|
|
93
|
+
return [
|
|
94
|
+
{ kind: "activity", components: manifest.activities },
|
|
95
|
+
{ kind: "activity-alias", components: manifest.activityAliases ?? [] },
|
|
96
|
+
{ kind: "service", components: manifest.services },
|
|
97
|
+
{ kind: "receiver", components: manifest.receivers },
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
export function auditAndroidIntentFilters(detection, manifests) {
|
|
101
|
+
const findings = [];
|
|
102
|
+
const evidence = [];
|
|
103
|
+
const warnings = [];
|
|
104
|
+
for (const entry of manifests) {
|
|
105
|
+
let filterCount = 0;
|
|
106
|
+
for (const group of componentGroups(entry.manifest)) {
|
|
107
|
+
for (const component of group.components) {
|
|
108
|
+
for (const filter of component.intentFilters) {
|
|
109
|
+
filterCount += 1;
|
|
110
|
+
auditFilter(entry, group.kind, component, filter, findings);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (filterCount > 0) {
|
|
115
|
+
evidence.push(`${entry.manifestPath}: ${filterCount} intent-filter(s) inspected`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return buildAndroidManifestCheckResult({
|
|
119
|
+
id: "android-intent-filters-audit",
|
|
120
|
+
category: "android-intent-filters",
|
|
121
|
+
title: "Android intent-filter audit",
|
|
122
|
+
detection,
|
|
123
|
+
manifests,
|
|
124
|
+
findings,
|
|
125
|
+
evidence,
|
|
126
|
+
warnings,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { makeAndroidFinding } from "./androidFinding.js";
|
|
2
|
+
import { buildAndroidManifestCheckResult } from "./checkResultBuilder.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.0 Batch 3 — initial Android permission audit.
|
|
5
|
+
//
|
|
6
|
+
// Conservative by design (agents.txt Batch 3 section 7.13): a declared
|
|
7
|
+
// permission is not automatically a vulnerability. Ordinary connectivity
|
|
8
|
+
// permissions (INTERNET etc.) never produce a finding. Recognized sensitive
|
|
9
|
+
// permission groups produce a "minor" (review-required) finding. Unknown/
|
|
10
|
+
// custom permissions produce an "informational" review finding. The only
|
|
11
|
+
// escalation is foreground + background location declared together, which
|
|
12
|
+
// still stays at "minor" (a legitimate, common pattern — not proof of
|
|
13
|
+
// misuse).
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const LOW_RISK_PERMISSIONS = new Set([
|
|
16
|
+
"android.permission.INTERNET",
|
|
17
|
+
"android.permission.ACCESS_NETWORK_STATE",
|
|
18
|
+
"android.permission.ACCESS_WIFI_STATE",
|
|
19
|
+
"android.permission.VIBRATE",
|
|
20
|
+
"android.permission.WAKE_LOCK",
|
|
21
|
+
"android.permission.RECEIVE_BOOT_COMPLETED",
|
|
22
|
+
]);
|
|
23
|
+
const SENSITIVE_PERMISSION_GROUPS = {
|
|
24
|
+
"android.permission.ACCESS_FINE_LOCATION": "location",
|
|
25
|
+
"android.permission.ACCESS_COARSE_LOCATION": "location",
|
|
26
|
+
"android.permission.ACCESS_BACKGROUND_LOCATION": "background-location",
|
|
27
|
+
"android.permission.CAMERA": "camera",
|
|
28
|
+
"android.permission.RECORD_AUDIO": "microphone",
|
|
29
|
+
"android.permission.READ_CONTACTS": "contacts",
|
|
30
|
+
"android.permission.WRITE_CONTACTS": "contacts",
|
|
31
|
+
"android.permission.GET_ACCOUNTS": "contacts",
|
|
32
|
+
"android.permission.READ_CALENDAR": "calendar",
|
|
33
|
+
"android.permission.WRITE_CALENDAR": "calendar",
|
|
34
|
+
"android.permission.SEND_SMS": "sms",
|
|
35
|
+
"android.permission.READ_SMS": "sms",
|
|
36
|
+
"android.permission.RECEIVE_SMS": "sms",
|
|
37
|
+
"android.permission.RECEIVE_MMS": "sms",
|
|
38
|
+
"android.permission.READ_CALL_LOG": "call-log",
|
|
39
|
+
"android.permission.WRITE_CALL_LOG": "call-log",
|
|
40
|
+
"android.permission.PROCESS_OUTGOING_CALLS": "call-log",
|
|
41
|
+
"android.permission.READ_PHONE_STATE": "phone",
|
|
42
|
+
"android.permission.CALL_PHONE": "phone",
|
|
43
|
+
"android.permission.BLUETOOTH_CONNECT": "nearby-devices",
|
|
44
|
+
"android.permission.BLUETOOTH_SCAN": "nearby-devices",
|
|
45
|
+
"android.permission.BLUETOOTH_ADVERTISE": "nearby-devices",
|
|
46
|
+
"android.permission.NEARBY_WIFI_DEVICES": "nearby-devices",
|
|
47
|
+
"android.permission.READ_EXTERNAL_STORAGE": "storage-legacy",
|
|
48
|
+
"android.permission.WRITE_EXTERNAL_STORAGE": "storage-legacy",
|
|
49
|
+
"android.permission.MANAGE_EXTERNAL_STORAGE": "storage-legacy",
|
|
50
|
+
"android.permission.READ_MEDIA_IMAGES": "media",
|
|
51
|
+
"android.permission.READ_MEDIA_VIDEO": "media",
|
|
52
|
+
"android.permission.READ_MEDIA_AUDIO": "media",
|
|
53
|
+
"android.permission.POST_NOTIFICATIONS": "notifications",
|
|
54
|
+
"android.permission.BODY_SENSORS": "body-sensors",
|
|
55
|
+
"android.permission.BODY_SENSORS_BACKGROUND": "body-sensors",
|
|
56
|
+
"android.permission.ACTIVITY_RECOGNITION": "activity-recognition",
|
|
57
|
+
"android.permission.FOREGROUND_SERVICE": "foreground-service",
|
|
58
|
+
"android.permission.REQUEST_INSTALL_PACKAGES": "package-install",
|
|
59
|
+
"android.permission.QUERY_ALL_PACKAGES": "package-visibility",
|
|
60
|
+
};
|
|
61
|
+
export function auditAndroidPermissions(detection, manifests) {
|
|
62
|
+
const findings = [];
|
|
63
|
+
const evidence = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
for (const entry of manifests) {
|
|
66
|
+
const declaredNames = new Set(entry.manifest.permissions.map((p) => p.name));
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const permission of entry.manifest.permissions) {
|
|
69
|
+
if (seen.has(permission.name))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(permission.name);
|
|
72
|
+
if (LOW_RISK_PERMISSIONS.has(permission.name)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const group = SENSITIVE_PERMISSION_GROUPS[permission.name];
|
|
76
|
+
const duplicateCount = entry.manifest.permissions.filter((p) => p.name === permission.name).length;
|
|
77
|
+
const duplicateNote = duplicateCount > 1 ? [`declared ${duplicateCount} times (deduplicated for this finding)`] : [];
|
|
78
|
+
if (group === "background-location") {
|
|
79
|
+
const hasForeground = declaredNames.has("android.permission.ACCESS_FINE_LOCATION") || declaredNames.has("android.permission.ACCESS_COARSE_LOCATION");
|
|
80
|
+
findings.push(makeAndroidFinding({
|
|
81
|
+
ruleId: hasForeground ? "android-permission-background-and-foreground-location" : "android-permission-sensitive",
|
|
82
|
+
title: hasForeground
|
|
83
|
+
? "Background location declared alongside foreground location"
|
|
84
|
+
: `Sensitive permission declared: ${permission.name}`,
|
|
85
|
+
severity: "minor",
|
|
86
|
+
confidence: "high",
|
|
87
|
+
description: hasForeground
|
|
88
|
+
? "The manifest declares ACCESS_BACKGROUND_LOCATION together with a foreground location permission. This is a common, legitimate pattern, but background location access typically requires explicit justification for store review."
|
|
89
|
+
: `The manifest declares the sensitive permission ${permission.name} (group: ${group}). This is not automatically a vulnerability; review that the app's functionality justifies the request.`,
|
|
90
|
+
manifestPath: entry.manifestPath,
|
|
91
|
+
identity: permission.name,
|
|
92
|
+
location: permission.location,
|
|
93
|
+
evidenceDetails: [`sourceElement=${permission.sourceElement ?? "uses-permission"}`, `group=${group}`, ...duplicateNote],
|
|
94
|
+
recommendation: "Confirm the permission is required for a real user-facing feature and document the justification.",
|
|
95
|
+
}));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (group) {
|
|
99
|
+
findings.push(makeAndroidFinding({
|
|
100
|
+
ruleId: "android-permission-sensitive",
|
|
101
|
+
title: `Sensitive permission declared: ${permission.name}`,
|
|
102
|
+
severity: "minor",
|
|
103
|
+
confidence: "high",
|
|
104
|
+
description: `The manifest declares the sensitive permission ${permission.name} (group: ${group}). This is not automatically a vulnerability; review that the app's functionality justifies the request.`,
|
|
105
|
+
manifestPath: entry.manifestPath,
|
|
106
|
+
identity: permission.name,
|
|
107
|
+
location: permission.location,
|
|
108
|
+
evidenceDetails: [`sourceElement=${permission.sourceElement ?? "uses-permission"}`, `group=${group}`, ...duplicateNote],
|
|
109
|
+
recommendation: "Confirm the permission is required for a real user-facing feature and document the justification.",
|
|
110
|
+
}));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!permission.name.startsWith("android.permission.")) {
|
|
114
|
+
findings.push(makeAndroidFinding({
|
|
115
|
+
ruleId: "android-permission-unknown-custom",
|
|
116
|
+
title: `Unknown custom permission declared: ${permission.name}`,
|
|
117
|
+
severity: "informational",
|
|
118
|
+
confidence: "high",
|
|
119
|
+
description: `The manifest declares a custom permission "${permission.name}" that is not a recognized platform permission. This is not automatically dangerous; review its purpose and, if it protects app components, whether its protectionLevel is appropriate.`,
|
|
120
|
+
manifestPath: entry.manifestPath,
|
|
121
|
+
identity: permission.name,
|
|
122
|
+
location: permission.location,
|
|
123
|
+
evidenceDetails: [`sourceElement=${permission.sourceElement ?? "uses-permission"}`, ...duplicateNote],
|
|
124
|
+
recommendation: "Review the custom permission's purpose and protection level.",
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (entry.manifest.permissions.length > 0) {
|
|
129
|
+
evidence.push(`${entry.manifestPath}: ${entry.manifest.permissions.length} permission declaration(s) inspected`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return buildAndroidManifestCheckResult({
|
|
133
|
+
id: "android-permissions-audit",
|
|
134
|
+
category: "android-permissions",
|
|
135
|
+
title: "Android permission audit",
|
|
136
|
+
detection,
|
|
137
|
+
manifests,
|
|
138
|
+
findings,
|
|
139
|
+
evidence,
|
|
140
|
+
warnings,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createAndroidProfile } from "../profile.js";
|
|
2
|
+
import { createAndroidTargetMetadata } from "../targetMetadata.js";
|
|
3
|
+
import { detectAndroidProject } from "./detectAndroidProject.js";
|
|
4
|
+
export function detectAndroidTarget(local) {
|
|
5
|
+
const detection = detectAndroidProject(local.targetRoot);
|
|
6
|
+
const androidProfile = createAndroidProfile({
|
|
7
|
+
detectionConfidence: detection.confidence,
|
|
8
|
+
evidence: detection.evidence,
|
|
9
|
+
unsupportedNotes: detection.warnings,
|
|
10
|
+
projectType: detection.projectKind,
|
|
11
|
+
projectSubtype: detection.uiToolkit,
|
|
12
|
+
supportedCapabilities: ["static-detection"],
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
detection,
|
|
16
|
+
target: createAndroidTargetMetadata({ local, androidProfile, detection }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.0 Batch 2 — bounded, deterministic build.gradle(.kts) evidence
|
|
3
|
+
// extraction.
|
|
4
|
+
//
|
|
5
|
+
// Extracts only the lexical signals needed for detection and classification
|
|
6
|
+
// (plugin ids, namespace, applicationId, Compose build-feature/dependency
|
|
7
|
+
// evidence). This is not a Gradle metadata parser (that is Batch 4's scope):
|
|
8
|
+
// values that cannot be statically read as a literal are preserved as
|
|
9
|
+
// "unresolved" evidence rather than guessed.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const ANDROID_APPLICATION_PLUGIN_PATTERN = /(['"`])com\.android\.application\1|id\s*\(\s*['"`]com\.android\.application['"`]\s*\)/;
|
|
12
|
+
const ANDROID_LIBRARY_PLUGIN_PATTERN = /(['"`])com\.android\.library\1|id\s*\(\s*['"`]com\.android\.library['"`]\s*\)/;
|
|
13
|
+
const KOTLIN_ANDROID_PLUGIN_PATTERN = /(['"`])org\.jetbrains\.kotlin\.android\1|kotlin\s*\(\s*['"`]android['"`]\s*\)|(['"`])kotlin-android\2/;
|
|
14
|
+
const NAMESPACE_LITERAL_PATTERN = /\bnamespace\s*[=]?\s*['"]([^'"]+)['"]/;
|
|
15
|
+
const NAMESPACE_UNRESOLVED_PATTERN = /\bnamespace\s*[=]?\s*(?!['"])[^\s\n]/;
|
|
16
|
+
const APPLICATION_ID_LITERAL_PATTERN = /\bapplicationId\s*[=]?\s*['"]([^'"]+)['"]/;
|
|
17
|
+
const APPLICATION_ID_UNRESOLVED_PATTERN = /\bapplicationId\s*[=]?\s*(?!['"])[^\s\n]/;
|
|
18
|
+
const COMPOSE_BUILD_FEATURE_PATTERN = /\bcompose\s*[=]?\s*true\b/;
|
|
19
|
+
const COMPOSE_DEPENDENCY_PATTERN = /androidx\.compose|compose-bom|compose\.(?:ui|material|material3|runtime|foundation)/;
|
|
20
|
+
const COMPOSE_PLUGIN_PATTERN = /org\.jetbrains\.kotlin\.plugin\.compose|com\.android\.compose|compose-compiler/;
|
|
21
|
+
export function extractBuildFileEvidence(text) {
|
|
22
|
+
const namespaceLiteral = text.match(NAMESPACE_LITERAL_PATTERN);
|
|
23
|
+
const applicationIdLiteral = text.match(APPLICATION_ID_LITERAL_PATTERN);
|
|
24
|
+
return {
|
|
25
|
+
androidApplicationPlugin: ANDROID_APPLICATION_PLUGIN_PATTERN.test(text),
|
|
26
|
+
androidLibraryPlugin: ANDROID_LIBRARY_PLUGIN_PATTERN.test(text),
|
|
27
|
+
kotlinAndroidPlugin: KOTLIN_ANDROID_PLUGIN_PATTERN.test(text),
|
|
28
|
+
namespace: namespaceLiteral?.[1],
|
|
29
|
+
namespaceUnresolved: !namespaceLiteral && NAMESPACE_UNRESOLVED_PATTERN.test(text),
|
|
30
|
+
applicationId: applicationIdLiteral?.[1],
|
|
31
|
+
applicationIdUnresolved: !applicationIdLiteral && APPLICATION_ID_UNRESOLVED_PATTERN.test(text),
|
|
32
|
+
composeBuildFeatureEvidence: COMPOSE_BUILD_FEATURE_PATTERN.test(text),
|
|
33
|
+
composeDependencyEvidence: COMPOSE_DEPENDENCY_PATTERN.test(text),
|
|
34
|
+
composePluginEvidence: COMPOSE_PLUGIN_PATTERN.test(text),
|
|
35
|
+
};
|
|
36
|
+
}
|