Install
openclaw skills install @ahmadhussain0335/mobile-eu-complianceAssesses Android, iOS, Flutter and connected-device companion apps against the EU Cyber Resilience Act, the Radio Equipment Directive delegated act with EN 18031-1, -2 and -3, and GDPR with ePrivacy: inventory, requirement mapping, evidence, gaps and a remediation plan in a gap report table. Engineering guidance, not legal advice. Use for CRA or EN 18031 readiness, SBOM and vulnerability handling, BLE or Wi-Fi companion apps, or GDPR reviews of mobile apps.
openclaw skills install @ahmadhussain0335/mobile-eu-complianceTurn EU product rules into engineering work for a mobile app or a connected device's companion app: find out which rules apply, map the code and processes to each requirement, collect evidence, and list the gaps with concrete fixes and owners.
This is engineering guidance, not legal advice. The agent states what the code and documents show and which requirement they relate to. Scope decisions, conformity routes and anything contractual go to the team's compliance or legal owner. Where a point cannot be verified, the agent says so instead of guessing.
Not for: a general security audit with MASVS findings (use mobile-security, then map its findings here),
store submission rules (use mobile-release), or accessibility law (use mobile-accessibility).
Checked against the Official Journal texts listed in docs/SOURCES.md of this pack. Re-check before
quoting them in customer-facing documents.
| Rule | Fact |
|---|---|
| Cyber Resilience Act, Regulation (EU) 2024/2847 | Applies from 11 December 2027. Article 14 (reporting) applies from 11 September 2026; Chapter IV (notified bodies) from 11 June 2026 (Art. 71) |
| CRA reporting (Art. 14) | Actively exploited vulnerability and severe incident: early warning within 24 hours, notification within 72 hours, to the coordinating CSIRT and ENISA via the single reporting platform. Final report: 14 days after a fix is available (vulnerability), one month after the notification (incident) |
| CRA support period (Art. 13(8)) | At least five years, or the expected use time if the product is expected to be in use for less |
| CRA SBOM (Annex I Part II(1)) | "commonly used and machine-readable format covering at the very least the top-level dependencies" |
| RED delegated act, Regulation (EU) 2022/30 | Applies from 1 August 2025 (date set by Delegated Regulation (EU) 2023/2444) |
| EN 18031-1 / -2 / -3:2024 | Cited with restrictions by Implementing Decision (EU) 2025/138 of 28 January 2025 for RED Art. 3(3)(d), (e) and (f) |
| GDPR breach notification (Art. 33) | To the supervisory authority without undue delay and, where feasible, within 72 hours |
| ePrivacy Directive Art. 5(3) | Storing or reading information on the user's device needs consent, unless strictly necessary for the service the user explicitly requested |
Package.resolved, pubspec.lock, Gradle dependencies).The team must be able to ship a fix and get it installed. A store update alone is not enough if old builds keep working with a known vulnerability.
// BAD: no way to stop a vulnerable build; users stay on it for years
class App : Application()
// GOOD: server-driven minimum version, checked on start, plus Play In-App Updates for the immediate flow
if (BuildConfig.VERSION_CODE < remoteConfig.minSupportedVersionCode) {
appUpdateManager.startUpdateFlowForResult(info, launcher, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build())
}
// GOOD: compare against a minimum build served by your backend and block with an update prompt
let build = Int(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "") ?? 0
if build < config.minimumSupportedBuild {
state = .updateRequired(appStoreURL)
}
Implementing Decision (EU) 2025/138 removes the presumption of conformity for all three EN 18031 parts if the user is allowed not to set and use any password.
// BAD: every device ships with the same PIN, and onboarding offers "Skip"
const val DEFAULT_PAIRING_PIN = "000000"
fun onSkipPasswordClicked() = finishOnboarding()
// GOOD: per-device secret printed on the label or set during onboarding; the step cannot be skipped
fun onSetPassword(password: String) {
require(passwordPolicy.isAcceptable(password)) { "rejected by policy" }
device.setAdminPassword(password)
}
CRC16 or CRC32 detects transmission errors. It does not stop a modified image.
// BAD: app downloads over HTTP and the device only checks a CRC
val image = httpClient.get("http://fw.example.com/latest.bin")
dfu.send(image, crc32(image))
// GOOD: HTTPS download, signed image, signature verified by the bootloader with a key the app never holds
val image = api.downloadFirmware(release.id) // TLS, pinned to your backend if required
require(sha256(image).contentEquals(release.sha256)) // integrity of the download
dfu.send(image) // device verifies the vendor signature before applying
// BAD
Telemetry(email = user.email, mac = device.macAddress, gps = location, rssi = rssi)
// GOOD
Telemetry(installId = rotatingInstallId, firmware = device.firmwareVersion, rssi = rssi)
// BAD
analytics.log('pair_ok', {'serial': device.serial, 'user': user.email});
// GOOD
analytics.log('pair_ok', {'model': device.model});
// BAD: analytics and ad SDKs start in Application.onCreate for every user
override fun onCreate() { super.onCreate(); Analytics.init(this); Ads.init(this) }
// GOOD: start only after the user agreed; strictly necessary storage (session, settings) needs no consent
fun onConsentChanged(consent: Consent) {
if (consent.analytics) Analytics.init(context) else Analytics.disable(context)
}
// GOOD: same rule on iOS; App Tracking Transparency is an additional Apple requirement, not GDPR consent
if consentStore.analyticsAllowed { Analytics.start() }
// BAD: "Delete account" only clears local preferences
fun deleteAccount() = prefs.edit { clear() }
// GOOD: server-side deletion, device unpairing and factory reset command, then local wipe
suspend fun deleteAccount() {
api.deleteAccount() // backend deletes account and device records
device.sendFactoryResetIfConnected()
localStore.wipe() // databases, files, Keystore aliases, caches
}
// BAD: no record of security events, or a record full of personal data
Log.d("Auth", "login ok for $email token=$token")
// GOOD: structured security events, no identifiers beyond what is needed, opt-out respected
securityLog.record(SecurityEvent.PairingFailed(reason = Reason.WrongPin, attempt = 3))
// GOOD: build.gradle.kts, CycloneDX Gradle plugin (pin the version in libs.versions.toml)
plugins { id("org.cyclonedx.bom") }
// ./gradlew cyclonedxBom -> archive the generated BOM with the release
Package.resolved, Podfile.lock or pubspec.lock with an SBOM tool
that reads them, in CI, per release tag. Keep the SBOM with the build, not only in a dashboard.<!-- BAD: no SECURITY.md, no contact in the app or on the website -->
<!-- GOOD: SECURITY.md in the repository, the same contact on the product website and in the app's About screen -->
## Reporting a vulnerability
Email security@example.com or use https://example.com/security. We acknowledge within 5 working days.
Supported versions and the support end date: see https://example.com/support-period.
This is a process, so the deliverable is a runbook, not code.
BAD: "Security issues go to the backlog."
GOOD: Runbook: triage owner on call; if exploitation is confirmed, early warning to the coordinating CSIRT
and ENISA via the single reporting platform within 24 h, notification within 72 h, final report
14 days after the fix ships; inform affected users about the issue and mitigations.
## EU compliance gap report: <product / app / version>
Engineering assessment, not legal advice. Scope and conformity decisions belong to <compliance owner>.
Scope: <app, device model, backend>. Role: <manufacturer | importer | distributor>.
Applies: CRA <yes | open question>, RED delegated act <yes | no | open question>, GDPR <yes>, ePrivacy 5(3) <yes>.
Key dates: CRA reporting 11 Sep 2026; CRA full application 11 Dec 2027; RED delegated act applies since 1 Aug 2025.
| Requirement | Status | Evidence | Gap | Fix | Owner |
|---|---|---|---|---|---|
| CRA Annex I Part I(c) security updates | Partial | Play In-App Updates in `UpdateManager.kt:31`; no minimum-version gate | Old builds keep working after a security fix | Server-driven minimum version check (rule 1) | Mobile lead |
| EN 18031-1 6.2.5 passwords / CRA I.I(d) | Missing | Onboarding has a Skip button (`OnboardingScreen.kt:88`) | User can keep no password; no presumption of conformity | Remove Skip, enforce password policy | Product + firmware |
| CRA Annex I Part II(1) SBOM | Missing | No SBOM step in `.github/workflows/release.yml` | No SBOM per release | Add CycloneDX generation and archive | DevOps |
| GDPR Art. 5(1)(c) minimisation | Met | `Telemetry.kt` sends model, firmware, RSSI only | - | - | - |
| ePrivacy Art. 5(3) consent | Not applicable | No analytics or ad SDKs in `build.gradle.kts` | - | - | - |
### Open questions for the compliance owner
- Is the device "internet-connected radio equipment" when it reaches the cloud only through the app?
### Remediation plan
1. <fix> -- owner, target date, deadline driver (for example CRA Art. 14 from 11 Sep 2026)
### Not assessed
- Backend and cloud infrastructure, device firmware internals, contracts with SDK vendors, <other>
Status values: Met, Partial, Missing, Not applicable. "Met" always has evidence.
mobile-security -- technical MASVS audit whose findings feed this gap reportmobile-ble -- pairing, secure update and protocol work on the app side of a devicemobile-ci-cd -- SBOM generation, dependency scanning and signed release pipelinesmobile-release -- store privacy declarations, account deletion and release processmobile-merge-request -- describing compliance-relevant changes in the MRmobile-accessibility -- the European Accessibility Act, which this skill does not cover