fix(igel): a trailing slash cost the version, and no version costs everything
Four findings from one estate, and three of them are the same shape: the scan
had nothing to compare, so it said nothing, and nothing reads as clean.
* IMI GET /firmwares/ answers HTTP 404 on UMS 12 ("No endpoint GET
/umsapi/v3/firmwares/"). The documented URL has no trailing slash. The
firmware table is the ONLY place a device's version string lives, so the
slash cost every version in the estate at once.
* IGEL OS 12 states its base-system build inline — 12.6.0+2 — and
_clean_version's dotted-numeric rule threw the whole string away. No
version, no scan: eleven devices on 12.6.0+2 showed zero CVEs while
sitting inside the range of both current ISNs. Semver excludes build
metadata from precedence and every IGEL bound is written without it
(12.7.6, 12.8.3), so the suffix is dropped, not rejected. 12.9.0+3 stays
clean, which is the correct answer and not a miss — 12.9.0 IS the fix.
* The UMS server's own version came back empty and its build not at all,
so the server asset carried no version and Test read "UMS (build ?)".
Nothing failed: serverstatus answers 200 either way. The documented IMI v3
keys are tried first, then the spellings UMS 12 has been seen to use, and
a payload that carries none of them now logs the keys it did carry — the
next rename should cost one log line, not an estate.
The fourth is not a bug but the question the CVEs are a footnote to.
IGEL OS 11 stops receiving security fixes on 2026-06-30. No CVE feed will ever
state that, because "unpatchable from here on" is not a CVE, and
endoflife.date carries no IGEL product at all — so the dates are transcribed
from IGEL's own Knowledge Base.
The mapping is the part that needed care, because IGEL's two terms are not the
two this codebase already has, and taken the obvious way round they invert:
EOL - End of Life no further ENHANCEMENTS; security fixes still ship.
OS 11 hit this in April 2023 when OS 12 launched,
and stayed patched for three more years. -> eoasFrom,
informational, LOW.
EOM - End of Maintenance "no updates, no security and bug fixes." -> eolFrom,
a real finding.
The 2025-12-31 that circulates for OS 11 is IGEL's original date; the vendor
page now states 30th June, 2026. Third-party migration write-ups still carry
the old one. The vendor's page wins.
OS 12 gets an entry with no EOM, because IGEL has published none and an
invented date would be the only unsourced one on the page. It raises nothing.
UMS 6 (EOM 2023-10-31) does — that one is 1037 days past.
A migrated device has no other way out of its old finding: a thin client has
no software inventory, so the EOL sweep's own reconcile never reaches it. Both
slugs are single-release, and the pass supersedes explicitly when the line it
now runs is one that raises nothing.
Findings say "IGEL product lifecycle", not endoflife.date. A row that named a
source which has never heard of the product sends an operator to a page that
cannot confirm or refute it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ API reference: https://kb.igel.com/en/igel-management-interface/current
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -112,12 +112,21 @@ class IgelClient:
|
||||
if r.status_code >= 400:
|
||||
raise IgelError(f"IMI serverstatus failed (HTTP {r.status_code}): {_imi_error(r)}")
|
||||
d = r.json() if r.content else {}
|
||||
return {
|
||||
"version": d.get("rmGuiServerVersion"),
|
||||
"build": d.get("buildNumber"),
|
||||
"server_uuid": d.get("serverUUID"),
|
||||
out = {
|
||||
"version": _first(d, _STATUS_VERSION_KEYS),
|
||||
"build": _first(d, _STATUS_BUILD_KEYS),
|
||||
"server_uuid": _first(d, ("serverUUID", "serverUuid", "uuid")),
|
||||
"server": d.get("server"),
|
||||
}
|
||||
if not out["version"]:
|
||||
# Said out loud, with the keys the server actually sent. The field
|
||||
# names below are IMI v3's documented ones and UMS answers 200
|
||||
# either way, so a rename shows up as an asset with no version and
|
||||
# a Test button reading "UMS (build ?)" — nothing failing, just
|
||||
# nothing there. The next rename should be one log line to find.
|
||||
logger.warning("IMI serverstatus carried no version; keys were: %s",
|
||||
sorted(d) if isinstance(d, dict) else type(d).__name__)
|
||||
return out
|
||||
|
||||
def get_firmwares(self) -> Dict[str, dict]:
|
||||
"""{firmware id: {product, version, type}}.
|
||||
@@ -129,7 +138,11 @@ class IgelClient:
|
||||
accepted too, because the same endpoint is documented both ways across
|
||||
IMI versions and an unwrapped answer must not read as "no firmwares".
|
||||
"""
|
||||
data = self._get("/firmwares/")
|
||||
# No trailing slash. UMS 12 routes "/firmwares/" to nothing and answers
|
||||
# HTTP 404 "No endpoint GET /umsapi/v3/firmwares/" — the documented URL
|
||||
# is /v3/firmwares, and the slash was silently costing the whole
|
||||
# firmware table, which is the only place a version string lives.
|
||||
data = self._get("/firmwares")
|
||||
rows = data.get("FwResource", []) if isinstance(data, dict) else (data or [])
|
||||
out: Dict[str, dict] = {}
|
||||
for row in rows:
|
||||
@@ -197,6 +210,25 @@ class IgelClient:
|
||||
}
|
||||
|
||||
|
||||
# Documented IMI v3 spelling first, then the ones UMS 12 has been seen to use.
|
||||
# https://kb.igel.com/en/igel-management-interface/current/get-v3-serverstatus
|
||||
_STATUS_VERSION_KEYS = ("rmGuiServerVersion", "umsVersion", "serverVersion",
|
||||
"version")
|
||||
_STATUS_BUILD_KEYS = ("buildNumber", "build", "buildNo", "buildnumber")
|
||||
|
||||
|
||||
def _first(d: object, keys) -> Optional[str]:
|
||||
"""First key present with a non-empty value, stringified. Empty is missing:
|
||||
an UMS that answers "" for its version must not read as a version of ""."""
|
||||
if not isinstance(d, dict):
|
||||
return None
|
||||
for k in keys:
|
||||
v = d.get(k)
|
||||
if v is not None and str(v).strip():
|
||||
return str(v).strip()
|
||||
return None
|
||||
|
||||
|
||||
def _imi_error(r: httpx.Response) -> str:
|
||||
"""The IMI error message, which lives in the body, not the status line."""
|
||||
try:
|
||||
|
||||
@@ -540,7 +540,14 @@ def _clean_version(v: str) -> Optional[str]:
|
||||
v = (v or "").strip()
|
||||
if not v or ":" in v:
|
||||
return None
|
||||
core = re.split(r"[ (]", v, 1)[0]
|
||||
# "+build" is dropped, not rejected. IGEL OS 12 states its base-system
|
||||
# build that way — 12.6.0+2 — and so does anything else using semver build
|
||||
# metadata, which the spec explicitly excludes from precedence. Every
|
||||
# advisory bound is written without it (IGEL's fixes are 12.7.6 and
|
||||
# 12.8.3), so keeping the suffix could only ever fail to match. It used to
|
||||
# fail the dotted-numeric rule outright, which meant no version, which
|
||||
# meant no scan: a whole IGEL OS 12 fleet showed zero CVEs.
|
||||
core = re.split(r"[ (+]", v, 1)[0]
|
||||
# Checkmk numbers its patches inline — 2.4.0p12 — and both NVD and
|
||||
# cvelistV5 state their bounds the same way (lessThan "2.4.0p13"). The
|
||||
# dotted-numeric rule threw the whole version away, so the agent was
|
||||
|
||||
@@ -865,6 +865,10 @@ _SINGLE_RELEASE_SLUGS = {
|
||||
# version, so an upgrade off a dead line must retire the old finding
|
||||
# (7.0 → 8.0 leaves no 7.0 install behind).
|
||||
"esxi", "vcenter",
|
||||
# A thin client boots exactly one IGEL OS and a UMS server runs one suite
|
||||
# version, so an 11 -> 12 migration must retire the OS 11 finding instead
|
||||
# of leaving it open next to the OS 12 asset it no longer describes.
|
||||
"igel-os", "igel-ums",
|
||||
}
|
||||
|
||||
|
||||
@@ -1063,9 +1067,17 @@ def upsert_eol_vulnerability(
|
||||
# Microsoft's lifecycle export — so a row whose id read EOL-MS-LIFECYCLE-…
|
||||
# claimed endoflife.date as its evidence, and operators could not tell
|
||||
# which source to check the date against.
|
||||
from_msl = (status.product_slug or "") == "ms-lifecycle"
|
||||
source_name = "Microsoft product lifecycle" if from_msl else "endoflife.date"
|
||||
source_key = "ms-lifecycle" if from_msl else "endoflife.date"
|
||||
slug_ = status.product_slug or ""
|
||||
if slug_ == "ms-lifecycle":
|
||||
source_name, source_key = "Microsoft product lifecycle", "ms-lifecycle"
|
||||
elif slug_.startswith("igel-"):
|
||||
# IGEL publishes its dates as prose in the Knowledge Base and nowhere
|
||||
# else — endoflife.date carries no IGEL product at all. A finding that
|
||||
# named endoflife.date as its evidence would point an operator at a
|
||||
# page that has never heard of the product.
|
||||
source_name, source_key = "IGEL product lifecycle", "igel-lifecycle"
|
||||
else:
|
||||
source_name, source_key = "endoflife.date", "endoflife.date"
|
||||
|
||||
existing = (
|
||||
db.query(Vulnerability)
|
||||
@@ -1104,7 +1116,7 @@ def upsert_eol_vulnerability(
|
||||
if vendor:
|
||||
existing.package_vendor = vendor[:255]
|
||||
from app.services.audit_events import reopen_if_patched
|
||||
reopen_if_patched(db, existing, reason="endoflife.date check reports this product as EOL again", source="eol_check")
|
||||
reopen_if_patched(db, existing, reason=f"{source_name} reports this product as EOL again", source="eol_check")
|
||||
# Resync bumps detected_at so the Newly EOL/EOS widget ranks the
|
||||
# freshest finding first.
|
||||
existing.detected_at = datetime.now()
|
||||
|
||||
@@ -15,11 +15,11 @@ The firmware version is the point, and it needs a join: the device record
|
||||
carries a `firmwareID`, and the version lives in the firmware table. One extra
|
||||
request for the whole estate — see igel_client.
|
||||
|
||||
Deliberately NOT here: an EOL check. endoflife.date carries no IGEL product at
|
||||
all (verified against its full product list), and IGEL publishes its lifecycle
|
||||
dates only as prose in the Knowledge Base. Inventing dates for the most
|
||||
consequential lifecycle question in this estate — IGEL OS 11 is end-of-
|
||||
maintenance — would be worse than saying nothing. The CVE findings still land.
|
||||
Lifecycle: endoflife.date carries no IGEL product at all (verified against its
|
||||
full product list), so the dates are transcribed from IGEL's own Knowledge Base
|
||||
into _IGEL_LIFECYCLE below. That is the most consequential question this estate
|
||||
has — IGEL OS 11 stops getting security fixes on 2026-06-30 — and no CVE feed
|
||||
will ever state it, because "unpatchable from here on" is not a CVE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -40,6 +40,90 @@ UMS_OS = "IGEL Universal Management Suite"
|
||||
IGEL_OS = "IGEL OS"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Product lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
# IGEL publishes lifecycle dates as an HTML table in the Knowledge Base and
|
||||
# nowhere machine-readable, so they are transcribed here. Source, both the
|
||||
# table and the term definitions:
|
||||
# https://kb.igel.com/en/igel-product-information/current/igel-product-lifecycle
|
||||
#
|
||||
# IGEL's two terms are NOT the two endoflife.date ones, and mapping them the
|
||||
# obvious way round gets the severity backwards:
|
||||
#
|
||||
# EOL - End of Life no further ENHANCEMENTS; security fixes still
|
||||
# ship. IGEL OS 11 hit this in April 2023, when
|
||||
# OS 12 launched, and stayed patched for 3 years.
|
||||
# -> eoasFrom (informational, LOW).
|
||||
# EOM - End of Maintenance "no updates, no security and bug fixes. The
|
||||
# product is no longer supported." THIS is the
|
||||
# date that matters. -> eolFrom (a real finding).
|
||||
#
|
||||
# Month-granularity entries are stored as the LAST day of that month: the
|
||||
# reading that never calls a product dead earlier than it is.
|
||||
#
|
||||
# The widely-circulated 2025-12-31 for OS 11 EOM is stale — it was IGEL's
|
||||
# original date and the vendor page now states 30th June, 2026. Third-party
|
||||
# migration write-ups still carry the old one; the vendor's page wins.
|
||||
_IGEL_LIFECYCLE: dict = {
|
||||
# (slug, release major): (eoas = IGEL "EOL", eol = IGEL "EOM", successor)
|
||||
("igel-os", 11): ("2023-04-30", "2026-06-30", "IGEL OS 12"),
|
||||
# OS 12 reaches IGEL-EOL in December 2029; no EOM is published yet, and an
|
||||
# invented one would be the only unsourced date on this page. None here
|
||||
# means "still maintained" and raises nothing.
|
||||
("igel-os", 12): ("2029-12-31", None, None),
|
||||
("igel-ums", 6): ("2023-04-30", "2023-10-31", "IGEL UMS 12"),
|
||||
("igel-ums", 12): (None, None, None),
|
||||
}
|
||||
|
||||
# Not modelled, deliberately: IGEL OS for Raspberry Pi 4 (EOM 2024-03-31) is a
|
||||
# separate line whose version string is indistinguishable from the x86 one —
|
||||
# only the device model tells them apart, and reading a model string to age a
|
||||
# device would misjudge every estate that has none.
|
||||
|
||||
|
||||
def igel_lifecycle(os_name: Optional[str], os_version: Optional[str]):
|
||||
"""(slug, release, EOLStatus | None) for an IGEL asset, or None.
|
||||
|
||||
The release comes from the VERSION, never from the product name — UMS has
|
||||
called the same OS three different things over its life. Returns a status
|
||||
of None for a release line that is still maintained (or unknown), and the
|
||||
caller still needs the slug+release in that case: that is what retires the
|
||||
finding of the line a migrated device has left.
|
||||
"""
|
||||
from app.services import cvelistv5_scan_service as c5
|
||||
from app.services import eol_service
|
||||
|
||||
name = (os_name or "").strip()
|
||||
if c5.IGEL_OS_RE.match(name):
|
||||
slug = "igel-os"
|
||||
elif name.lower().startswith(UMS_OS.lower()):
|
||||
slug = "igel-ums"
|
||||
else:
|
||||
return None
|
||||
release = c5.igel_release(os_version or "")
|
||||
if release is None:
|
||||
return None
|
||||
dates = _IGEL_LIFECYCLE.get((slug, release))
|
||||
if not dates:
|
||||
# A release line IGEL has not published dates for. Unknown, not
|
||||
# supported — say nothing rather than guess.
|
||||
return (slug, release, None)
|
||||
eoas, eom, successor = dates
|
||||
if not eoas and not eom:
|
||||
return (slug, release, None)
|
||||
rel = {
|
||||
"name": str(release),
|
||||
"label": str(release),
|
||||
"eoasFrom": eoas,
|
||||
"eolFrom": eom,
|
||||
"isMaintained": not eol_service._past(eom),
|
||||
}
|
||||
if successor:
|
||||
rel["latest"] = {"name": successor}
|
||||
return (slug, release, eol_service._build_eol_status(rel, slug))
|
||||
|
||||
|
||||
def load_igel_config(db: Session) -> Optional[dict]:
|
||||
"""Decrypt + parse igel_config, or None when not configured."""
|
||||
from app.auth.setting_crypto import read_setting_value
|
||||
@@ -211,6 +295,7 @@ def _run_igel_sync_locked(db: Session, cfg: dict) -> dict:
|
||||
sync_devices = bool(cfg.get("sync_devices", True))
|
||||
stats = {"devices": 0, "assets_matched": 0, "assets_created": 0,
|
||||
"unknown_firmware": 0, "non_igel_os": 0, "cve_findings": 0,
|
||||
"eol_findings": 0,
|
||||
"assets_inactivated": 0, "assets_reactivated": 0, "errors": []}
|
||||
seen_asset_ids: set = set()
|
||||
|
||||
@@ -315,6 +400,11 @@ def _run_igel_sync_locked(db: Session, cfg: dict) -> dict:
|
||||
except Exception as e:
|
||||
stats["errors"].append(f"cve scan: {e}")
|
||||
|
||||
try:
|
||||
stats["eol_findings"] = _run_eol_scan(db, seen_asset_ids)
|
||||
except Exception as e:
|
||||
stats["errors"].append(f"eol scan: {e}")
|
||||
|
||||
try:
|
||||
recon = reconcile_igel_by_seen_ids(
|
||||
db, seen_asset_ids=seen_asset_ids,
|
||||
@@ -327,13 +417,55 @@ def _run_igel_sync_locked(db: Session, cfg: dict) -> dict:
|
||||
|
||||
logger.info(
|
||||
"IGEL sync done: %d devices, %d matched, %d created, %d unknown firmware, "
|
||||
"%d non-IGEL-OS, %d CVE findings, %d inactivated, %d reactivated",
|
||||
"%d non-IGEL-OS, %d CVE findings, %d EOL findings, %d inactivated, "
|
||||
"%d reactivated",
|
||||
stats["devices"], stats["assets_matched"], stats["assets_created"],
|
||||
stats["unknown_firmware"], stats["non_igel_os"], stats["cve_findings"],
|
||||
stats["assets_inactivated"], stats["assets_reactivated"])
|
||||
stats["eol_findings"], stats["assets_inactivated"],
|
||||
stats["assets_reactivated"])
|
||||
return stats
|
||||
|
||||
|
||||
def _run_eol_scan(db: Session, asset_ids: set) -> int:
|
||||
"""Lifecycle pass over the assets this sync touched.
|
||||
|
||||
Separate from the CVE pass because it answers a different question. A CVE
|
||||
says one hole is open and names the version that closes it; EOM says no
|
||||
version will ever close the next one. On an estate sitting on IGEL OS 11
|
||||
that is the finding every individual CVE is a footnote to.
|
||||
"""
|
||||
if not asset_ids:
|
||||
return 0
|
||||
from app.services import eol_service
|
||||
total = 0
|
||||
for asset in db.query(Asset).filter(Asset.id.in_(asset_ids)).all():
|
||||
try:
|
||||
resolved = igel_lifecycle(asset.operating_system, asset.os_version)
|
||||
if not resolved:
|
||||
continue
|
||||
slug, release, status = resolved
|
||||
if status and (status.is_eol or status.is_eol_soon or status.is_eoas):
|
||||
eol_service.upsert_eol_vulnerability(
|
||||
db, asset_id=asset.id,
|
||||
product_name=(asset.operating_system or IGEL_OS).strip(),
|
||||
installed_version=(asset.os_version or str(release)),
|
||||
status=status, vendor="IGEL")
|
||||
total += 1
|
||||
else:
|
||||
# This line is maintained (or IGEL states no dates for it), so
|
||||
# there is nothing to raise — but the device may have got here
|
||||
# by migrating off one that was not, and that finding has no
|
||||
# other way out. A thin client has no software inventory, so
|
||||
# the EOL sweep's own reconcile never reaches it.
|
||||
eol_service._supersede_old_eol(
|
||||
db, asset.id, slug,
|
||||
eol_service._pseudo_cve_id(slug, str(release)))
|
||||
except Exception as e:
|
||||
logger.warning("IGEL EOL check failed for %s: %s", asset.hostname, e)
|
||||
db.commit()
|
||||
return total
|
||||
|
||||
|
||||
def _run_cve_scan(db: Session, asset_ids: set) -> int:
|
||||
"""CVE pass over the assets this sync touched — both IGEL paths.
|
||||
|
||||
|
||||
@@ -683,7 +683,7 @@ function IgelCard() {
|
||||
const r = await api.post('/api/v1/integrations/igel/test');
|
||||
const u = r.data?.ums;
|
||||
setStatus(r.data?.ok && !r.data?.error
|
||||
? { message: `OK — UMS ${u?.version ?? '?'} (build ${u?.build ?? '?'}), ${r.data.device_count ?? '?'} devices, ${r.data.firmware_count ?? '?'} firmwares`, type: 'success' }
|
||||
? { message: `OK — UMS ${u?.version || '?'} (build ${u?.build || '?'}), ${r.data.device_count ?? '?'} devices, ${r.data.firmware_count ?? '?'} firmwares`, type: 'success' }
|
||||
: { message: `Failed at ${r.data?.step}: ${r.data?.error}`, type: 'error' });
|
||||
} catch (e: any) {
|
||||
setStatus({ message: e?.response?.data?.detail || 'Test failed', type: 'error' });
|
||||
|
||||
@@ -211,8 +211,67 @@ def demo():
|
||||
assert cpe._os_family("IGEL OS") == "linux"
|
||||
assert cpe._os_family("IGEL Universal Management Suite") is None
|
||||
|
||||
# --- IGEL OS 12's "+build" suffix ------------------------------------
|
||||
# COSMOS states the base-system build inline: 12.6.0+2. _clean_version's
|
||||
# dotted-numeric rule threw the whole string away, so the version was
|
||||
# None, so no path scanned the device at all — a fleet on 12.6.0+2 read as
|
||||
# zero findings. Semver says build metadata is not part of precedence and
|
||||
# every IGEL bound is written without it, so it is dropped.
|
||||
assert cpe._clean_version("12.6.0+2") == "12.6.0"
|
||||
assert cpe._clean_version("12.9.0+3") == "12.9.0"
|
||||
assert [c for c, *_ in _scan(index, "12.6.0+2")] == [
|
||||
"CVE-2026-82017", "CVE-2026-82018"]
|
||||
# 12.9.0+3 is above both fixes (12.7.6 / 12.8.3). Clean is the right
|
||||
# answer here, not a miss — the build suffix must not change that either.
|
||||
assert _scan(index, "12.9.0+3") == []
|
||||
|
||||
print("IGEL OS detection OK")
|
||||
|
||||
|
||||
def demo_lifecycle():
|
||||
"""IGEL's own lifecycle terms, mapped onto the EOL model.
|
||||
|
||||
IGEL publishes no machine-readable dates, so _IGEL_LIFECYCLE transcribes
|
||||
the Knowledge Base table. The mapping is the part worth pinning: IGEL's
|
||||
"EOL" still ships security fixes and IGEL's "EOM" is where they stop, so
|
||||
EOM — and only EOM — may raise a real finding.
|
||||
"""
|
||||
from app.services.igel_service import igel_lifecycle
|
||||
|
||||
slug, rel, st = igel_lifecycle("IGEL OS", "11.08.100.01")
|
||||
assert (slug, rel) == ("igel-os", 11), (slug, rel)
|
||||
assert st.is_eol and st.eol_date == "2026-06-30", st # EOM, not EOL
|
||||
assert st.is_eoas and st.eoas_date == "2023-04-30", st # IGEL's "EOL"
|
||||
# The finding has to name the way out, and for OS 11 that is not a version.
|
||||
assert st.latest_version == "IGEL OS 12", st
|
||||
|
||||
# OS 12: IGEL states an end-of-life (Dec 2029) and no EOM at all. An
|
||||
# invented EOM would be the one unsourced date on the page, so a device on
|
||||
# 12 raises nothing — including one whose version carries a build suffix.
|
||||
for v in ("12.7.5", "12.6.0+2", "12.9.0+3"):
|
||||
slug, rel, st = igel_lifecycle("IGEL OS", v)
|
||||
assert (slug, rel) == ("igel-os", 12), (v, slug, rel)
|
||||
assert st is None or not (st.is_eol or st.is_eol_soon or st.is_eoas), (v, st)
|
||||
|
||||
# The UMS server is its own product on its own dates. UMS 6 went EOM in
|
||||
# 2023 and the estate's UMS 12 is current.
|
||||
_, _, st6 = igel_lifecycle("IGEL Universal Management Suite", "6.10.130")
|
||||
assert st6.is_eol and st6.eol_date == "2023-10-31", st6
|
||||
assert igel_lifecycle("IGEL Universal Management Suite", "12.12.100")[2] is None
|
||||
|
||||
# Neither an unrelated OS nor a versionless asset gets a verdict.
|
||||
assert igel_lifecycle("Windows 10", "10.0.19045") is None
|
||||
assert igel_lifecycle("IGEL OS", "") is None
|
||||
|
||||
# The finding must not claim endoflife.date as its evidence — it has never
|
||||
# heard of IGEL, which is the whole reason the table exists.
|
||||
from app.services import eol_service
|
||||
assert "igel-os" in eol_service._SINGLE_RELEASE_SLUGS
|
||||
assert "igel-ums" in eol_service._SINGLE_RELEASE_SLUGS
|
||||
|
||||
print("IGEL lifecycle OK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo()
|
||||
demo_lifecycle()
|
||||
|
||||
@@ -69,7 +69,10 @@ def _handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, json={"message": "Logout successful"})
|
||||
if path.endswith("/serverstatus"):
|
||||
return httpx.Response(200, json=SERVERSTATUS)
|
||||
if path.endswith("/firmwares/"):
|
||||
# No trailing slash — UMS 12 answers "/firmwares/" with a 404, and this
|
||||
# stub does the same. The slash cost the whole firmware table, and with it
|
||||
# every version string in the estate.
|
||||
if path.endswith("/firmwares"):
|
||||
return httpx.Response(200, json=FIRMWARES)
|
||||
if path.endswith("/thinclients"):
|
||||
return httpx.Response(200, json=DEVICES)
|
||||
@@ -95,11 +98,30 @@ def demo():
|
||||
|
||||
fws = c.get_firmwares()
|
||||
assert set(fws) == {"2", "7", "9"}
|
||||
assert c._client.base_url.path.rstrip("/").endswith("/umsapi/v3")
|
||||
assert fws["2"]["version"] == "11.08.230.01" and fws["2"]["type"] == "LX"
|
||||
assert fws["7"]["type"] == "OS12"
|
||||
# Returned verbatim: the client does not decide what IGEL OS is.
|
||||
assert fws["9"]["type"] == "W10"
|
||||
|
||||
# A UMS that spells the version differently still answers. The keys
|
||||
# below are IMI v3's documented ones, UMS returns 200 either way, and
|
||||
# the only symptom of a rename is an asset with no version at all.
|
||||
c2 = _client()
|
||||
c2._client = httpx.Client(
|
||||
base_url=c2.base_url,
|
||||
transport=httpx.MockTransport(lambda r: httpx.Response(
|
||||
200, json={"umsVersion": "12.12.100", "build": "45123"})))
|
||||
alt = c2.get_server_status()
|
||||
assert (alt["version"], alt["build"]) == ("12.12.100", "45123"), alt
|
||||
# An empty string is missing, not a version of "".
|
||||
c3 = _client()
|
||||
c3._client = httpx.Client(
|
||||
base_url=c3.base_url,
|
||||
transport=httpx.MockTransport(lambda r: httpx.Response(
|
||||
200, json={"rmGuiServerVersion": "", "buildNumber": ""})))
|
||||
assert c3.get_server_status()["version"] is None
|
||||
|
||||
devices = c.get_devices()
|
||||
# The recycle-bin entry is gone; the three live ones remain.
|
||||
assert [d["unit_id"] for d in devices] == [
|
||||
|
||||
Reference in New Issue
Block a user