From 5809092175a3dc830fcfe39183f1ba91c9d23efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sat, 17 Oct 2026 21:35:41 +0300 Subject: [PATCH] Files view: the change-details modal shows the full file path (v2.4.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the modal header showed only the flat name (EF_ICCID, 4F09); it now shows the path built from data already available: the tracker's DF identity named via KNOWN_FIDS (MF, DF_GSM_ACCESS, DF_TELECOM, …), 'ADF' for the current application (7FFF), DF_CHILDREN for DF-qualified EF names (4F52 → EF_KcGPRS) and the EF.DIR labels for AID-selected applications — e.g. `MF/EF_ICCID (2FE2)`, `ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID A00000015141434C00/4F09` - exposed as `path` in the change-details payload and in the files entries (the timeline rows keep the short name — they have no room) - the colour legend (changed / newly observed / never observed) moved below the two hex views - tests: `_file_path` cases (MF, ADF, DF-qualified EF, AID with and without an EF.DIR label, FID-as-name) plus the payload field, and the Node modal test asserts the path header and that the legend follows the views - v2.4.2; SW cache simtrace-analyser-v68 --- README.md | 13 ++++++++----- README_RUS.md | 7 +++++-- frontend/index.html | 18 +++++++++--------- frontend/sw.js | 2 +- pyproject.toml | 2 +- simtrace_analyser/filetrack.py | 26 +++++++++++++++++++++++++- simtrace_analyser/version.py | 2 +- tests/test_filetrack.py | 26 ++++++++++++++++++++++++++ tests/test_pcap_import.py | 2 +- 9 files changed, 77 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 2e1a851..c3474a4 100644 --- a/README.md +++ b/README.md @@ -194,11 +194,14 @@ trace-wide timeline of file operations — every **Read**, **Update** and **Changed on card** row shows the same file name as the Map view, and clicking a row jumps to the APDU in the trace. Rows that carry a recorded change (a card-side change, or an update that wrote or first observed bytes) also offer -**Details**: a modal showing the affected unit — the whole file, or the single -record that changed — **before and after** the change, as hex views (16 bytes -per row, never-observed bytes drawn as `--`) with the bytes that changed -highlighted in one colour and the bytes the operation first observed in -another, plus the changed/new byte counts and the APDU reference. +**Details**: a modal showing the full file path (`MF/EF_ICCID (2FE2)`, +`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — the DF +named from the EF.DIR labels when known) and the affected unit — the whole +file, or the single record that changed — **before and after** the change, as +hex views (16 bytes per row, never-observed bytes drawn as `--`) with the bytes +that changed highlighted in one colour and the bytes the operation first +observed in another, the changed/new byte counts, the APDU reference and the +colour legend below the views. The images come from `GET /api/sessions//files/image?key=…&change=…` and are reconstructed from the current image by undoing the later changes diff --git a/README_RUS.md b/README_RUS.md index 43a4ed7..9cc8a87 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -199,11 +199,14 @@ Files — это таймлайн файловых операций по все **Update** и **Changed on card** показывают те же имена файлов, что и вид Map, а щелчок по строке переходит к APDU в трассе. У строк с записанным изменением (изменение со стороны карты либо запись, изменившая или впервые показавшая -байты) есть кнопка **Details**: окно показывает затронутую единицу — файл +байты) есть кнопка **Details**: окно показывает полный путь файла (`MF/EF_ICCID (2FE2)`, +`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — DF +называется по меткам из EF.DIR, если они есть) и затронутую единицу — файл целиком или изменившуюся запись — **до и после** изменения в виде hex-вида (16 байт в строке, никогда не наблюдавшиеся байты — `--`), причём изменённые байты подсвечены одним цветом, а впервые наблюдаемые — другим; ниже — -счётчики изменённых/новых байт и ссылка на APDU. +счётчики изменённых/новых байт, ссылка на APDU и легенда цветов под +представлениями. Изображения отдаёт `GET /api/sessions//files/image?key=…&change=…`; они восстанавливаются из текущего образа отменой более поздних изменений diff --git a/frontend/index.html b/frontend/index.html index 9169e3c..0f826e1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -192,7 +192,7 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(- .file-op .file-op-diff:hover { color: var(--warn); } .file-diff-modal { max-width: 760px; } .file-diff-body { max-height: 60vh; overflow-y: auto; margin-bottom: 16px; } -.hex-legend { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); margin-bottom: 6px; flex-wrap: wrap; } +.hex-legend { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); margin-top: 10px; flex-wrap: wrap; } .hex-summary { font-size: 12px; color: var(--text-muted); margin-bottom: 10px; } .hex-pair { display: flex; gap: 16px; flex-wrap: wrap; } .hex-pair > div { flex: 1 1 320px; min-width: 0; } @@ -2244,18 +2244,18 @@ function renderFileDiff(image) { } if (image.index !== null && image.index !== undefined) meta.push(`#${image.index}`); if (image.elapsed !== null && image.elapsed !== undefined) meta.push(fmt_elapsed(image.elapsed)); - el('file-diff-sub').textContent = `${image.name || ''} — ${meta.join(' · ')}`; + el('file-diff-sub').textContent = `${image.path || image.name || ''} — ${meta.join(' · ')}`; el('file-diff-body').innerHTML = - `
- aa ${t('changed')} - aa ${t('newly observed')} - -- ${t('never observed')} -
-
${t('{c} bytes changed, {n} bytes newly observed') + `
${t('{c} bytes changed, {n} bytes newly observed') .replace('{c}', changed).replace('{n}', fresh)}

${t('Before')}

${hexImageHtml(image, 'before')}

${t('After')}

${hexImageHtml(image, 'after')}
+
+
+ aa ${t('changed')} + aa ${t('newly observed')} + -- ${t('never observed')}
` + (image.image_truncated ? `

${t('The image is frozen (size cap reached) — later changes are not tracked.')}

` : ''); } @@ -3167,7 +3167,7 @@ function onFilterChange() { } // ──────────────────── Versioning ──────────────────── -const PWA_VERSION = '2.4.1'; +const PWA_VERSION = '2.4.2'; const MIN_SERVER_VERSION = '2.3.0'; function parseVersion(v) { diff --git a/frontend/sw.js b/frontend/sw.js index d1fe6a4..968f3e8 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simtrace-analyser-v67'; +const CACHE = 'simtrace-analyser-v68'; const PRECACHE = [ 'index.html', diff --git a/pyproject.toml b/pyproject.toml index 41276e8..4fca784 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "simtrace-analyser" -version = "2.4.1" +version = "2.4.2" description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "GPL-2.0-or-later"} diff --git a/simtrace_analyser/filetrack.py b/simtrace_analyser/filetrack.py index 10f9ca7..0c811f4 100644 --- a/simtrace_analyser/filetrack.py +++ b/simtrace_analyser/filetrack.py @@ -39,7 +39,8 @@ keeps counting). """ from .database import _SUCCESS_SW1 -from .decode import (_is_df_fid, select_target_fid, selected_df_fid, sfi_table) +from .decode import (DF_CHILDREN, KNOWN_FIDS, _is_df_fid, select_target_fid, + selected_df_fid, sfi_table) IMAGE_CAP = 64 * 1024 # observed bytes kept per file EVENTS_CAP = 1000 # change events kept per file @@ -72,6 +73,25 @@ def _file_label(d): return None +def _file_path(df, fid, name, aid_labels=None): + """A human-readable path for a tracked file: the DF (named when known), + the EF and the FID — e.g. ``MF/EF_ICCID (2FE2)``, + ``ADF/EF_IMSI (6F07)``, ``DF_GSM_ACCESS/EF_KcGPRS (4F52)``, + ``AID A00000015141434C00/4F09``.""" + ef = (DF_CHILDREN.get(df, {}) or {}).get(fid) or name or fid.upper() + if df.startswith('aid:'): + aid = df[4:].upper() + label = (aid_labels or {}).get(aid) + df_label = f'AID {aid}' + (f' ({label})' if label else '') + elif df == '7fff': + df_label = 'ADF' + else: + df_label = KNOWN_FIDS.get(df) or df.upper() + if ef.upper() == fid.upper(): + return f'{df_label}/{ef.upper()}' + return f'{df_label}/{ef} ({fid.upper()})' + + def _key_str(key): df, fid = key return f'{df}/{fid}' @@ -515,6 +535,8 @@ class FileTracker: return { 'key': _key_str(entry.key), 'name': entry.name, + 'path': _file_path(entry.df, entry.fid, entry.name, + self.aid_labels), 'df': entry.df, 'fid': entry.fid, 'structure': entry.structure, @@ -540,6 +562,8 @@ class FileTracker: for key, entry in self.files.items(): files[_key_str(key)] = { 'name': entry.name, + 'path': _file_path(entry.df, entry.fid, entry.name, + self.aid_labels), 'df': entry.df, 'fid': entry.fid, 'sfi': entry.sfi, diff --git a/simtrace_analyser/version.py b/simtrace_analyser/version.py index e756343..d9645f0 100644 --- a/simtrace_analyser/version.py +++ b/simtrace_analyser/version.py @@ -1,3 +1,3 @@ """Single source of truth for the SIMtrace Analyser version.""" -__version__ = '2.4.1' +__version__ = '2.4.2' diff --git a/tests/test_filetrack.py b/tests/test_filetrack.py index 9eed37f..d891b71 100644 --- a/tests/test_filetrack.py +++ b/tests/test_filetrack.py @@ -96,6 +96,7 @@ class TestCountersAndRegistration(TrackerCase): entry = data['files']['3f00/2fe2'] self.assertEqual(entry['counters'], {'select': 2, 'read': 1, 'update': 1}) self.assertEqual(entry['name'], 'EF_ICCID') + self.assertEqual(entry['path'], 'MF/EF_ICCID (2FE2)') self.assertEqual(entry['size'], 10) def test_failed_select_does_not_register(self): @@ -419,6 +420,7 @@ class TestChangeImages(TrackerCase): update_binary(0, 'AABB0000'), ) self.assertEqual(result['kind'], 'device') + self.assertEqual(result['path'], 'MF/EF_ICCID (2FE2)') self.assertEqual(result['record'], None) self.assertEqual(result['ranges'], [[2, 2]]) self.assertEqual(result['observed_ranges'], []) @@ -497,6 +499,30 @@ class TestChangeImages(TrackerCase): {'offset': 6, 'hex': 'cc00'}]) +class TestFilePath(unittest.TestCase): + def test_paths(self): + from simtrace_analyser.filetrack import _file_path + # MF-level EF: the DF is named, the EF is named, the FID is shown. + self.assertEqual(_file_path('3f00', '2fe2', 'EF_ICCID'), + 'MF/EF_ICCID (2FE2)') + # The ADF of the current application. + self.assertEqual(_file_path('7fff', '6f07', 'EF_IMSI'), + 'ADF/EF_IMSI (6F07)') + # A DF-qualified name from DF_CHILDREN wins over the flat FID. + self.assertEqual(_file_path('5f3b', '4f52', '4F52'), + 'DF_GSM_ACCESS/EF_KcGPRS (4F52)') + # An application selected by an unknown AID: the AID is the DF name, + # with the EF.DIR label when the trace surfaced it. + self.assertEqual(_file_path('aid:a00000015141434c00', '4f09', '4F09'), + 'AID A00000015141434C00/4F09') + self.assertEqual(_file_path('aid:a0000000871002ff49ff0589', '6f07', + 'EF_IMSI', + {'A0000000871002FF49FF0589': 'USIM'}), + 'AID A0000000871002FF49FF0589 (USIM)/EF_IMSI (6F07)') + # An unnamed file: the FID alone, no duplicated parentheses. + self.assertEqual(_file_path('7f10', '6f3a', '6F3A'), 'DF_TELECOM/6F3A') + + class TestEndpoint(unittest.TestCase): def test_session_files_endpoint(self): from simtrace_analyser.server import RequestHandler diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index 13748aa..7ab0251 100644 --- a/tests/test_pcap_import.py +++ b/tests/test_pcap_import.py @@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase): class TestVersion(unittest.TestCase): def test_package_version(self): from simtrace2_pysniff import __version__ - self.assertEqual(__version__, '2.4.1') + self.assertEqual(__version__, '2.4.2') def test_status_includes_version(self): import os