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:
1 parent
cbb73536f4
commit
5809092175
9 files changed
+77
-21
No files matched your search
@@ -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
|
**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
|
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
|
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
|
**Details**: a modal showing the full file path (`MF/EF_ICCID (2FE2)`,
|
||||||
record that changed — **before and after** the change, as hex views (16 bytes
|
`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — the DF
|
||||||
per row, never-observed bytes drawn as `--`) with the bytes that changed
|
named from the EF.DIR labels when known) and the affected unit — the whole
|
||||||
highlighted in one colour and the bytes the operation first observed in
|
file, or the single record that changed — **before and after** the change, as
|
||||||
another, plus the changed/new byte counts and the APDU reference.
|
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
|
The images come from `GET /api/sessions/<id>/files/image?key=…&change=…` and
|
||||||
are reconstructed from the current image by undoing the later changes
|
are reconstructed from the current image by undoing the later changes
|
||||||
|
|||||||
+5
-2
@@ -199,11 +199,14 @@ Files — это таймлайн файловых операций по все
|
|||||||
**Update** и **Changed on card** показывают те же имена файлов, что и вид Map,
|
**Update** и **Changed on card** показывают те же имена файлов, что и вид Map,
|
||||||
а щелчок по строке переходит к APDU в трассе. У строк с записанным изменением
|
а щелчок по строке переходит к APDU в трассе. У строк с записанным изменением
|
||||||
(изменение со стороны карты либо запись, изменившая или впервые показавшая
|
(изменение со стороны карты либо запись, изменившая или впервые показавшая
|
||||||
байты) есть кнопка **Details**: окно показывает затронутую единицу — файл
|
байты) есть кнопка **Details**: окно показывает полный путь файла (`MF/EF_ICCID (2FE2)`,
|
||||||
|
`ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — DF
|
||||||
|
называется по меткам из EF.DIR, если они есть) и затронутую единицу — файл
|
||||||
целиком или изменившуюся запись — **до и после** изменения в виде hex-вида
|
целиком или изменившуюся запись — **до и после** изменения в виде hex-вида
|
||||||
(16 байт в строке, никогда не наблюдавшиеся байты — `--`), причём изменённые
|
(16 байт в строке, никогда не наблюдавшиеся байты — `--`), причём изменённые
|
||||||
байты подсвечены одним цветом, а впервые наблюдаемые — другим; ниже —
|
байты подсвечены одним цветом, а впервые наблюдаемые — другим; ниже —
|
||||||
счётчики изменённых/новых байт и ссылка на APDU.
|
счётчики изменённых/новых байт, ссылка на APDU и легенда цветов под
|
||||||
|
представлениями.
|
||||||
|
|
||||||
Изображения отдаёт `GET /api/sessions/<id>/files/image?key=…&change=…`; они
|
Изображения отдаёт `GET /api/sessions/<id>/files/image?key=…&change=…`; они
|
||||||
восстанавливаются из текущего образа отменой более поздних изменений
|
восстанавливаются из текущего образа отменой более поздних изменений
|
||||||
|
|||||||
+9
-9
@@ -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-op .file-op-diff:hover { color: var(--warn); }
|
||||||
.file-diff-modal { max-width: 760px; }
|
.file-diff-modal { max-width: 760px; }
|
||||||
.file-diff-body { max-height: 60vh; overflow-y: auto; margin-bottom: 16px; }
|
.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-summary { font-size: 12px; color: var(--text-muted); margin-bottom: 10px; }
|
||||||
.hex-pair { display: flex; gap: 16px; flex-wrap: wrap; }
|
.hex-pair { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||||
.hex-pair > div { flex: 1 1 320px; min-width: 0; }
|
.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.index !== null && image.index !== undefined) meta.push(`#${image.index}`);
|
||||||
if (image.elapsed !== null && image.elapsed !== undefined) meta.push(fmt_elapsed(image.elapsed));
|
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 =
|
el('file-diff-body').innerHTML =
|
||||||
`<div class="hex-legend">
|
`<div class="hex-summary">${t('{c} bytes changed, {n} bytes newly observed')
|
||||||
<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')
|
|
||||||
.replace('{c}', changed).replace('{n}', fresh)}</div>
|
.replace('{c}', changed).replace('{n}', fresh)}</div>
|
||||||
<div class="hex-pair">
|
<div class="hex-pair">
|
||||||
<div><h4>${t('Before')}</h4>${hexImageHtml(image, 'before')}</div>
|
<div><h4>${t('Before')}</h4>${hexImageHtml(image, 'before')}</div>
|
||||||
<div><h4>${t('After')}</h4>${hexImageHtml(image, 'after')}</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>` +
|
</div>` +
|
||||||
(image.image_truncated ? `<p class="hex-note">${t('The image is frozen (size cap reached) — later changes are not tracked.')}</p>` : '');
|
(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 ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '2.4.1';
|
const PWA_VERSION = '2.4.2';
|
||||||
const MIN_SERVER_VERSION = '2.3.0';
|
const MIN_SERVER_VERSION = '2.3.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v67';
|
const CACHE = 'simtrace-analyser-v68';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace-analyser"
|
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)"
|
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
|
||||||
readme = {file = "README.md", content-type = "text/markdown"}
|
readme = {file = "README.md", content-type = "text/markdown"}
|
||||||
license = {text = "GPL-2.0-or-later"}
|
license = {text = "GPL-2.0-or-later"}
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ keeps counting).
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .database import _SUCCESS_SW1
|
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
|
IMAGE_CAP = 64 * 1024 # observed bytes kept per file
|
||||||
EVENTS_CAP = 1000 # change events kept per file
|
EVENTS_CAP = 1000 # change events kept per file
|
||||||
@@ -72,6 +73,25 @@ def _file_label(d):
|
|||||||
return None
|
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):
|
def _key_str(key):
|
||||||
df, fid = key
|
df, fid = key
|
||||||
return f'{df}/{fid}'
|
return f'{df}/{fid}'
|
||||||
@@ -515,6 +535,8 @@ class FileTracker:
|
|||||||
return {
|
return {
|
||||||
'key': _key_str(entry.key),
|
'key': _key_str(entry.key),
|
||||||
'name': entry.name,
|
'name': entry.name,
|
||||||
|
'path': _file_path(entry.df, entry.fid, entry.name,
|
||||||
|
self.aid_labels),
|
||||||
'df': entry.df,
|
'df': entry.df,
|
||||||
'fid': entry.fid,
|
'fid': entry.fid,
|
||||||
'structure': entry.structure,
|
'structure': entry.structure,
|
||||||
@@ -540,6 +562,8 @@ class FileTracker:
|
|||||||
for key, entry in self.files.items():
|
for key, entry in self.files.items():
|
||||||
files[_key_str(key)] = {
|
files[_key_str(key)] = {
|
||||||
'name': entry.name,
|
'name': entry.name,
|
||||||
|
'path': _file_path(entry.df, entry.fid, entry.name,
|
||||||
|
self.aid_labels),
|
||||||
'df': entry.df,
|
'df': entry.df,
|
||||||
'fid': entry.fid,
|
'fid': entry.fid,
|
||||||
'sfi': entry.sfi,
|
'sfi': entry.sfi,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Single source of truth for the SIMtrace Analyser version."""
|
"""Single source of truth for the SIMtrace Analyser version."""
|
||||||
|
|
||||||
__version__ = '2.4.1'
|
__version__ = '2.4.2'
|
||||||
@@ -96,6 +96,7 @@ class TestCountersAndRegistration(TrackerCase):
|
|||||||
entry = data['files']['3f00/2fe2']
|
entry = data['files']['3f00/2fe2']
|
||||||
self.assertEqual(entry['counters'], {'select': 2, 'read': 1, 'update': 1})
|
self.assertEqual(entry['counters'], {'select': 2, 'read': 1, 'update': 1})
|
||||||
self.assertEqual(entry['name'], 'EF_ICCID')
|
self.assertEqual(entry['name'], 'EF_ICCID')
|
||||||
|
self.assertEqual(entry['path'], 'MF/EF_ICCID (2FE2)')
|
||||||
self.assertEqual(entry['size'], 10)
|
self.assertEqual(entry['size'], 10)
|
||||||
|
|
||||||
def test_failed_select_does_not_register(self):
|
def test_failed_select_does_not_register(self):
|
||||||
@@ -419,6 +420,7 @@ class TestChangeImages(TrackerCase):
|
|||||||
update_binary(0, 'AABB0000'),
|
update_binary(0, 'AABB0000'),
|
||||||
)
|
)
|
||||||
self.assertEqual(result['kind'], 'device')
|
self.assertEqual(result['kind'], 'device')
|
||||||
|
self.assertEqual(result['path'], 'MF/EF_ICCID (2FE2)')
|
||||||
self.assertEqual(result['record'], None)
|
self.assertEqual(result['record'], None)
|
||||||
self.assertEqual(result['ranges'], [[2, 2]])
|
self.assertEqual(result['ranges'], [[2, 2]])
|
||||||
self.assertEqual(result['observed_ranges'], [])
|
self.assertEqual(result['observed_ranges'], [])
|
||||||
@@ -497,6 +499,30 @@ class TestChangeImages(TrackerCase):
|
|||||||
{'offset': 6, 'hex': 'cc00'}])
|
{'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):
|
class TestEndpoint(unittest.TestCase):
|
||||||
def test_session_files_endpoint(self):
|
def test_session_files_endpoint(self):
|
||||||
from simtrace_analyser.server import RequestHandler
|
from simtrace_analyser.server import RequestHandler
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
|
|||||||
class TestVersion(unittest.TestCase):
|
class TestVersion(unittest.TestCase):
|
||||||
def test_package_version(self):
|
def test_package_version(self):
|
||||||
from simtrace2_pysniff import __version__
|
from simtrace2_pysniff import __version__
|
||||||
self.assertEqual(__version__, '2.4.1')
|
self.assertEqual(__version__, '2.4.2')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user