Omahub
← All plugins
D

Ukraine Air Alert

by dfrost

Air raid alert status for selected regions of Ukraine, from the keyless siren.pp.ua mirror of the official UkraineAlarm API. Unofficial and passive: no notifications, no sound. Not a life-safety system.

Security review

Potentially dangerous behavior detected · 2 findings

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
19bd083
Scanned
1 month ago
  • high destructive_filesystem tests/test_model.js:177

    Destructive operation on the root filesystem or a block device.

    rm -rf /"));
  • Docs sudo README.md:102

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo or pkexec is required. There is no setuid

Automated analysis only — not a security guarantee.

AI advisory review

No obvious issues detected

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

Low
AI risk level
Low
Recommendation
install
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
19bd083
Reviewed
1 month ago

The deterministic scan flagged a destructive filesystem operation, but that is a test assertion in tests/test_model.js checking that a crafted region id like "31; rm -rf /" is rejected — it is not executable code. The plugin itself is a passive QML widget that fetches air alert data from a third-party API via a carefully bounded bash script, with strict input validation, response size caps, and no privileged operations or persistence beyond its own state/cache files. No real security risk was found.

  • The deterministic scan's high-risk finding is a false positive: the `rm -rf /` string appears only in a test case to verify input sanitization, not in any code path that runs on install or use.
  • The plugin depends on a third-party API (siren.pp.ua) and makes outbound HTTPS requests; while the code is defensive, the upstream service could be compromised or return malicious data, though the plugin caps and sanitizes all inputs.
  • The README mentions sudo only to state that it is not required; no elevated privileges are used.
How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/dfrost90/omarchy-ukraine-air-alert --enable
Widgets #bar #quickshell

Ukraine Air Alert

An Omarchy bar widget showing whether an air raid alert is active in the regions of Ukraine you care about, which type it is, and how long it has been running.

Ukraine Air Alert


⚠ Disclaimer — read this first

This is a convenience indicator. It is not a life-safety system, and it must not be relied on as one.

  • Not official. This plugin is not affiliated with, endorsed by, or operated by the State Emergency Service of Ukraine, the Armed Forces, any oblast administration, or any other authority. Nor is it affiliated with any air alert app, service or mirror, including the ones it reads from. It is an independent hobby project that reads a free, unofficial, community-run third-party mirror.
  • It can be wrong. Data can be delayed, incomplete, cached, or simply incorrect. The upstream service can go offline or return stale values with no indication that it has.
  • It can be silent when it matters. Your machine may be asleep, offline, locked, on a dead battery, or showing a fullscreen window over the bar. The widget raises no notification and makes no sound by design — it cannot wake you and does not try to.
  • Absence of an alert here does not mean you are safe. A network failure, a misconfigured region, or an upstream outage all look calmer than reality. The widget marks staleness where it can detect it, but it cannot detect everything.

Use official channels for decisions about your safety — the state air alert app, oblast and municipal channels, and the physical sirens. Treat this widget as a glance, never as the thing you act on.

Provided as-is, without warranty of any kind, as stated in the MIT licence.


What it deliberately does not do

No notifications. No sound.

People living under these alerts already have phone apps and the actual street sirens. A fourth thing shouting in the same room is noise, not signal. What a desktop can usefully add is a quiet, always-visible answer to "is it on right now, and how long has it been on" — so that is all this does.

Data source

siren.pp.ua — a free, keyless public mirror of the official UkraineAlarm API.

No API key, no registration, no account. That is why this source was chosen over alerts.in.ua, which has better data but issues per-user tokens by request form and rate-limits hard — every person installing this plugin would need their own token first.

Alert types reported: AIR, ARTILLERY, URBAN_FIGHTS, CHEMICAL, NUCLEAR, INFO.

Install

omarchy plugin add https://github.com/dfrost90/omarchy-ukraine-air-alert.git --enable

Then put it on the bar:

omarchy bar move io.github.dfrost90.air-alert --section right

Removal

omarchy plugin remove io.github.dfrost90.air-alert
rm -f ~/.local/state/omarchy/settings/air-alert.json
rm -rf ~/.cache/omarchy/air-alert

Remove its entry from bar.layout in ~/.config/omarchy/shell.json if you added one by hand.

Dependencies

Dependency Why
curl Every HTTP request.
jq JSON parsing and construction in scripts/fetch-alerts.

Both ship with Omarchy. There is no AUR package, no Python, and no compiled component.

Permissions, data access and safety

  • Network: outbound HTTPS to siren.pp.ua only. Nothing else is contacted.
  • What is sent: the region ids you selected, in the URL path. No identifiers, no telemetry, no analytics, no user agent beyond curl's default.
  • Privileges: none. No sudo or pkexec is required. There is no setuid binary, no privileged helper, and no system file is touched.
  • Files written: exactly two, both its own — ~/.local/state/omarchy/settings/air-alert.json (your region choice) and ~/.cache/omarchy/air-alert/regions.json (the cached region list). It never writes your Omarchy config; pinning regions in shell.json is something you do by hand, and the plugin only reads it.
  • What it reads: its own two files, plus ~/.local/state/omarchy/settings/weather.json — read once, only to pre-fill the region search box as a suggestion, and never acted on automatically.
  • Bounded input: every network response and every file read is capped before it is retained, and a body that reaches its ceiling is refused whole rather than truncated — a truncated prefix of a hostile response can still parse as valid JSON and then be believed. Region counts, alert counts and catalog size are capped as well as byte length, because shape says nothing about how many QML objects a payload will build.
  • Text sinks: every string that comes from the network or from a user-writable file is stripped of markup delimiters and truncated before it reaches a Text, including the tooltip — Ui/PanelToolTip.qml's contentItem sets no textFormat and therefore inherits AutoText.
  • Region ids are validated as digits before they reach a process argument or a URL.

Choosing regions

Click the widget and type a region name. Both scripts work:

Kyiv      → м. Київ, Київська область
Львів     → Львівська область, Львівський район
Odesa     → Одеська область, Одеський район

Latin input is matched by romanizing the region list (KMU 2010), because the API's region tree carries Ukrainian names only. Search also survives Ukrainian adjectival forms, so Odesa finds Одеська and Kremenchuk finds Кременчуцький.

Each result shows its type and parent oblast on a second line, since Львівська область and Львівський район are otherwise indistinguishable.

You can watch several regions at once. Each gets an editable display label — useful for showing an English name for a Ukrainian region — which never changes which region is actually monitored.

Which region the pill speaks for

With more than one region watched, a star appears beside each. The starred region is the one the pill shows when several are alerting at once.

It never silences anything. If your starred region is clear and another watched region is alerting, the pill shows that other region's alert, labelled so you can see it is not your primary. You chose to watch it; hiding it would be the same under-reporting the ~ marker exists to avoid.

Removing the starred region clears the star rather than leaving it dangling.

If your Omarchy weather location is set, the picker opens pre-filtered to it as a suggestion. It is never applied on its own. Measured against the live region list, romanized matching of 32 Ukrainian cities resolved 12 uniquely, 16 ambiguously and missed 4 — and for Kyiv specifically the match lands on Київська область rather than м. Київ, a different region with different alerts. The last click is yours.

Granularity is oblast and raion. Hromada-level regions exist in the API and can be pinned by id in shell.json, but they are kept out of the picker: 1455 near-identical names would bury the 151 that are useful.

Configuration

All optional. Pinning regions in shell.json overrides the picker.

{
  "id": "io.github.dfrost90.air-alert",
  "regions": [
    { "id": "31", "label": "Kyiv" },
    { "id": "36", "label": "Vinnytskyi" }
  ],
  "primaryId": "31",
  "pollSeconds": 90,
  "staleAfterSeconds": 180,
  "icon": "󰀦"
}
Key Default Meaning
regions (picker) Pinned regions, max 32. Overrides the picker's saved choice.
primaryId (picker) Region id the pill favours when several alert at once. Overrides the star, and hides it in the panel.
pollSeconds 90 Poll interval in seconds. Minimum 10.
staleAfterSeconds 60 How long without a successful fetch before the data counts as stale. Floored at pollSeconds * 2, so 180s at the default interval.
icon 󰀦 Bar glyph.
maxLabelWidth 110 Pixel ceiling on the region label in the pill before it elides. The alert type and elapsed time are never elided.

Region ids: ./scripts/fetch-alerts --regions | jq -r '.regions[] | "\(.id)\t\(.name)"'

Reading the pill

Shows Meaning
glyph only All watched regions clear, data fresh.
AIR 1h 24m Alert active, with type and elapsed time.
Kyiv AIR 1h 24m As above, labelled — shown when watching several regions.
~AIR 1h 24m Alert active, but the data is stale. ~ marks the elapsed time as extrapolated from the last successful fetch.
? Data is stale and the last known state was clear — the widget does not know.
set region No region chosen yet.

The important case is ~. If the network drops during an alert the widget keeps showing the alert rather than falling back to "unknown". Under-reporting an active alert is the dangerous direction; over-reporting one is merely annoying.

Hover for per-region detail and the age of the last successful update. Middle click forces a refresh.

How it polls

Every 90 seconds by default, and most ticks cost exactly one ~38-byte request to /api/v3/alerts/status — a single integer that changes whenever anything changes anywhere in Ukraine. Only when that integer moves does the widget fetch the watched regions.

Steady state is therefore well under one request per minute. That matters because this runs 24/7 against a free community service someone else pays for.

The trade is that an alert can be up to 90 seconds old before the pill changes. That is deliberate: this is a passive indicator you glance at, not something you find out from — see the disclaimer.

The mirror answers 429 after roughly five requests in quick succession, so consecutive failures double the poll interval (capped at 5 minutes) until one succeeds. A rate-limited or offline upstream is never polled at full speed.

Alert history is fetched only when the panel is opened, and cached for 60s. The panel lists the three most recent alerts per region plus a count over the last 24 hours — how many there have been says more than any single row.

Tests

bash tests/run

229 assertions: the fetch script against a stubbed curl (no request leaves the machine), and every pure function in Model.js under node.

Legal

Short version: the code is mine and MIT-licensed, the data is public information that Ukrainian law places outside copyright, and the endpoint is public and keyless. Nothing here is redistributed under someone else's terms.

The code. Every line in this repository is original work, licensed MIT. No third-party source is vendored, bundled or linked. In particular this plugin does not use uasiren, the Python client library for the same service — that repository ships no licence file, so its code would not be safe to reuse. This plugin talks to the HTTP endpoint directly, which is a different thing from reusing the client.

The data. Air raid alert records are bare facts — an alert is on, or it is not. Under Ukrainian copyright law they are not anyone's property to license:

  • Law No. 2811-IX On Copyright and Related Rights, Art. 8(1)(1) — copyright does not protect "повідомлення про новини або інші факти, що мають характер звичайної прес-інформації" (reports of news or other facts having the character of ordinary press information).
  • Art. 8(1)(3) puts acts and official documents of state bodies outside copyright as well.
  • The region catalogue is a database, so the sui generis database right in Art. 21(4) is the thing to check — and Art. 21(4) excludes it by its own terms for "бази, створеної для систематизації даних, що є публічною інформацією" (a database created to systematize data that is public information under the Access to Public Information Act). A catalogue of Ukrainian oblasts and raions used to route public civil-defence warnings is exactly that.

The endpoint. siren.pp.ua is published by Pavlo Annekov as an explicitly public wrapper around the official UkraineAlarm API. It requires no key, no account and no registration, and it publishes no terms of service to accept or breach. Its one stated limit is 50 requests per minute per IP. This plugin uses roughly 0.7 requests per minute at its default interval, and backs off on failure — see How it polls. The official UkraineAlarm API issues keys by request form; this plugin never contacts it directly, so no key terms bind it or you.

No marks, no emblems. This plugin uses no logo, wordmark, state symbol, military insignia or municipal coat of arms. Its name is plain description of what it shows. The bar glyph is a codepoint rendered from whatever Nerd Font you already have installed; no font is shipped here.

Privacy. No accounts, no telemetry, no analytics, no identifiers. The only thing this plugin sends is the numeric region ids you picked, in the URL path. Being honest about the one unavoidable exposure: any HTTP request reveals your IP address to the host that answers it, and siren.pp.ua is no exception. That is the same exposure as loading its website, and nothing beyond it is transmitted.

Warranty. None, and this matters more here than in most software. The MIT licence's "AS IS, WITHOUT WARRANTY OF ANY KIND" is not boilerplate in a tool that sits next to a life-safety question — read the disclaimer at the top and mean it.

Credits

  • Pavlo Annekov for running siren.pp.ua as a free public service. Not required to be credited; credited anyway, because someone pays for that server.
  • UkraineAlarm as the upstream source.

Licence

MIT — see LICENSE.