Files view: the change-details modal shows the full file path (v2.4.2)

- 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
This commit is contained in:
catarrh committed 2026-10-17 21:35:41 +03:00
1 parent cbb73536f4
commit 5809092175
9 files changed
+77 -21

No files matched your search

+8 -5
View File
@@ -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/<id>/files/image?key=…&change=…` and
are reconstructed from the current image by undoing the later changes
+5 -2
View File
@@ -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/<id>/files/image?key=…&change=…`; они
восстанавливаются из текущего образа отменой более поздних изменений
+9 -9
View File
@@ -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 =
`<div class="hex-legend">
<span class="hex-cell hex-changed">aa</span> ${t('changed')}
<span class="hex-cell hex-new">aa</span> ${t('newly observed')}
<span class="hex-cell hex-unknown">--</span> ${t('never observed')}
</div>
<div class="hex-summary">${t('{c} bytes changed, {n} bytes newly observed')
`<div class="hex-summary">${t('{c} bytes changed, {n} bytes newly observed')
.replace('{c}', changed).replace('{n}', fresh)}</div>
<div class="hex-pair">
<div><h4>${t('Before')}</h4>${hexImageHtml(image, 'before')}</div>
<div><h4>${t('After')}</h4>${hexImageHtml(image, 'after')}</div>
</div>
<div class="hex-legend">
<span class="hex-cell hex-changed">aa</span> ${t('changed')}
<span class="hex-cell hex-new">aa</span> ${t('newly observed')}
<span class="hex-cell hex-unknown">--</span> ${t('never observed')}
</div>` +
(image.image_truncated ? `<p class="hex-note">${t('The image is frozen (size cap reached) — later changes are not tracked.')}</p>` : '');
}
@@ -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) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simtrace-analyser-v67';
const CACHE = 'simtrace-analyser-v68';
const PRECACHE = [
'index.html',
+1 -1
View File
@@ -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"}
+25 -1
View File
@@ -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,
+1 -1
View File
@@ -1,3 +1,3 @@
"""Single source of truth for the SIMtrace Analyser version."""
__version__ = '2.4.1'
__version__ = '2.4.2'
+26
View File
@@ -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
+1 -1
View File
@@ -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