v3.25.4: the per-operation Contains match for the profile access rules

The access-rules check was exact-only: every listed decoded line had to be
on the card and every card rule listed (missing + extra both fail), so
"READ/WRITE must be always, the RESIZE condition is irrelevant" could not
be expressed.

A rule may now carry accessRules.mode: 'contains' (the editor's Match
selector: Exact default / Contains; the collapsed header badges
"· per operation"): every listed rule is matched per operation - each AM
bit / command entry (RESIZE/INCREASE) must be covered by a card rule with
the same SC list, whatever AM grouping the card uses
(fcpArrRuleOps/fcpArrRuleOpCovered/fcpArrRuleSatisfied) - and the rules not
listed are not checked (no extra side).  A form outside the supported
subset (a raw template form, a labelled pairs-form line) compares by exact
string; the proprietary (b8) form stays whole.  The check carries mode and
the report notes "compared per operation; other card rules are ignored".
Validation accepts mode in {exact, contains}; the capture keeps writing no
mode (exact).

Docs: help EN/RU.  Tests: the per-op matcher, the contains/exact verdicts
(the combined-AM grouping case, a condition mismatch, the RESIZE condition,
not-listed ignored), the editor selector/badge, the validation, the report
note.  886 frontend + 873 Python; sw cache simple-v428.
This commit is contained in:
2026-10-11 16:18:15 +03:00
parent 1aa24507fc
commit 2a58cfb235
7 changed files with 197 additions and 13 deletions
+1 -1
View File
@@ -469,7 +469,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Путь</strong> — начинается с <code class="font-mono text-sm">MF</code> (например, <code class="font-mono text-sm">MF/7F10/6F3A</code>) или с AID ADF (например, <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
<li><strong>Проверка FCP/FCI</strong> — какая часть информации об управлении файлом проверяется: <strong>Только тип файла (FCP)</strong> (существование + тип файла), <strong>Тип файла + размер (FCP)</strong> (добавляются размер файла либо длина/число записей для record-файлов) или <strong>Полный FCI</strong> (добавляется побайтовое сравнение сырого ответа SELECT — шаблона FCP <code class="font-mono text-sm">'62'</code> — выявляет изменения FID/AID, жизненного цикла, атрибутов безопасности и проприетарных параметров).</li>
<li><strong>Правила доступа</strong> &mdash; проверяет условия доступа файла как <em>декодированные правила</em>, независимо от того, на какую запись <code class="font-mono text-sm">EF.ARR</code> указывает ссылка атрибута безопасности (<code class="font-mono text-sm">'8B'</code>) в FCI: правила указанной записи разрешаются из <code class="font-mono text-sm">EF.ARR</code> карты (или снимка) и сравниваются как множество с захваченными строками. Захватываются автоматически при создании профиля с карты или из снимка для каждого файла, чей FCI содержит ссылку (собственное содержимое EF.ARR по умолчанию пропускается &mdash; значимая проверка это правила); в редакторе захваченные правила показываются структурированными строками &mdash; операция в виде флажков битов (или командной формы RESIZE FILE / INCREASE), а каждое условие безопасности — селектором (always, never или ссылка на ключ с её использованием), с предпросмотром канонической декодированной строки, которую сравнивает проверка; «+ правило» создаёт новое (READ always), а шаблонные формы вне этого набора остаются нередактируемыми строками с возможностью удаления. Переключатель «Нет/Проверять» отключает проверку, не теряя правила; блок сворачивается в заголовок со счётчиком правил, а «Нет» скрывает его полностью. Файл, чей FCI потерял ссылку, не проходит проверку; ссылка, которую не удалось разрешить (нет кэша <code class="font-mono text-sm">EF.ARR</code>, запись вне диапазона), помечается как непроверяемая. При <strong>Полном FCI</strong> необязательный флажок <em>«Игнорировать ссылку на EF.ARR в точном FCI»</em> сравнивает FCI без значения <code class="font-mono text-sm">'8B'</code>: другой номер записи больше не приводит к несовпадению, а все остальные DO по-прежнему сравниваются побайтово (смысл ссылки проверяет эта проверка).</li>
<li><strong>Правила доступа</strong> &mdash; проверяет условия доступа файла как <em>декодированные правила</em>, независимо от того, на какую запись <code class="font-mono text-sm">EF.ARR</code> указывает ссылка атрибута безопасности (<code class="font-mono text-sm">'8B'</code>) в FCI: правила указанной записи разрешаются из <code class="font-mono text-sm">EF.ARR</code> карты (или снимка) и сравниваются как множество с захваченными строками. Захватываются автоматически при создании профиля с карты или из снимка для каждого файла, чей FCI содержит ссылку (собственное содержимое EF.ARR по умолчанию пропускается &mdash; значимая проверка это правила); в редакторе захваченные правила показываются структурированными строками &mdash; операция в виде флажков битов (или командной формы RESIZE FILE / INCREASE), а каждое условие безопасности — селектором (always, never или ссылка на ключ с её использованием), с предпросмотром канонической декодированной строки, которую сравнивает проверка; «+ правило» создаёт новое (READ always), а шаблонные формы вне этого набора остаются нередактируемыми строками с возможностью удаления. Селектор <strong>«Сопоставление»</strong> выбирает сравнение: <strong>«Точное»</strong> (по умолчанию) требует полного совпадения правил карты с перечисленными строками; <strong>«Содержит»</strong> требует, чтобы каждое перечисленное правило было разрешено картой, — сравнение <em>по операциям</em> (каждый бит AM / командная запись, поэтому другое объединение битов в байте AM при той же политике тоже совпадёт), а не перечисленное правило не проверяется. Переключатель «Нет/Проверять» отключает проверку, не теряя правила; блок сворачивается в заголовок со счётчиком правил, а «Нет» скрывает его полностью. Файл, чей FCI потерял ссылку, не проходит проверку; ссылка, которую не удалось разрешить (нет кэша <code class="font-mono text-sm">EF.ARR</code>, запись вне диапазона), помечается как непроверяемая. При <strong>Полном FCI</strong> необязательный флажок <em>«Игнорировать ссылку на EF.ARR в точном FCI»</em> сравнивает FCI без значения <code class="font-mono text-sm">'8B'</code>: другой номер записи больше не приводит к несовпадению, а все остальные DO по-прежнему сравниваются побайтово (смысл ссылки проверяет эта проверка).</li>
<li><strong>Атрибуты файла</strong> — тип файла, размер, длина записи и число записей из FCP-шаблона (любой можно оставить незаданным).</li>
<li><strong>Проверить содержимое</strong> (опционально) — <strong>Точное</strong> (точное совпадение hex) или <strong>Маска</strong>, где <code class="font-mono text-sm">?</code> — шаблон на один полубайт (маска без <code class="font-mono text-sm">?</code> — совпадение префикса, например <code class="font-mono text-sm">0891</code> для MCC/MNC из IMSI). Для record-файлов хранится список по записям.</li>
</ul>
+1 -1
View File
@@ -468,7 +468,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Path</strong> — starts with <code class="font-mono text-sm">MF</code> (e.g. <code class="font-mono text-sm">MF/7F10/6F3A</code>) or an ADF AID (e.g. <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
<li><strong>FCP/FCI check</strong> — how much of the file control information to verify: <strong>Filetype only (FCP)</strong> (existence + file type), <strong>Filetype + size (FCP)</strong> (adds file size, or record length/count for record files), or <strong>Exact FCI</strong> (adds a byte-for-byte comparison of the raw SELECT response — the FCP template <code class="font-mono text-sm">'62'</code> — catching FID/AID, life-cycle status, security-attribute and proprietary-parameter changes).</li>
<li><strong>Access rules</strong> &mdash; verifies a file&rsquo;s access conditions as <em>decoded rules</em>, independent of which <code class="font-mono text-sm">EF.ARR</code> record the FCI&rsquo;s security-attribute reference (<code class="font-mono text-sm">'8B'</code>) points at: the referenced record&rsquo;s rules are resolved from the card&rsquo;s (or the snapshot&rsquo;s) <code class="font-mono text-sm">EF.ARR</code> and compared as a set with the captured lines. Captured automatically when a profile is created from a card or a snapshot for every file whose FCI carries a reference (EF.ARR&rsquo;s own contents are skipped by default &mdash; the rules are the meaningful check); the editor shows the captured rules as structured rows &mdash; the operation as bit checkboxes (or the RESIZE FILE / INCREASE command form) and each security condition as a select (always, never, or a key reference with its usage), with a preview line of the canonical decoded string the check compares; <em>+ rule</em> builds a new one (READ always), and the template forms outside this subset stay raw and removable. The None/Check toggle disables the check without losing the rules; the block collapses to a header with the rule count and None hides it entirely. A file whose FCI lost the reference fails; a reference that cannot be resolved (no cached <code class="font-mono text-sm">EF.ARR</code>, record out of range) is reported as unverifiable. With <strong>Exact FCI</strong>, the optional <em>Ignore the EF.ARR reference in the exact FCI</em> checkbox compares the FCI without the <code class="font-mono text-sm">'8B'</code> value: a different record number no longer fails while every other DO is still compared byte-for-byte (the reference&rsquo;s meaning is verified by this check).</li>
<li><strong>Access rules</strong> &mdash; verifies a file&rsquo;s access conditions as <em>decoded rules</em>, independent of which <code class="font-mono text-sm">EF.ARR</code> record the FCI&rsquo;s security-attribute reference (<code class="font-mono text-sm">'8B'</code>) points at: the referenced record&rsquo;s rules are resolved from the card&rsquo;s (or the snapshot&rsquo;s) <code class="font-mono text-sm">EF.ARR</code> and compared as a set with the captured lines. Captured automatically when a profile is created from a card or a snapshot for every file whose FCI carries a reference (EF.ARR&rsquo;s own contents are skipped by default &mdash; the rules are the meaningful check); the editor shows the captured rules as structured rows &mdash; the operation as bit checkboxes (or the RESIZE FILE / INCREASE command form) and each security condition as a select (always, never, or a key reference with its usage), with a preview line of the canonical decoded string the check compares; <em>+ rule</em> builds a new one (READ always), and the template forms outside this subset stay raw and removable. The <strong>Match</strong> selector picks the comparison: <strong>Exact</strong> (default) requires the card&rsquo;s rules to equal the listed lines; <strong>Contains</strong> requires every listed rule to be granted by the card, matched <em>per operation</em> (each AM bit / command entry, so a different AM grouping of the same policy still matches), and a rule not listed is not checked. The None/Check toggle disables the check without losing the rules; the block collapses to a header with the rule count and None hides it entirely. A file whose FCI lost the reference fails; a reference that cannot be resolved (no cached <code class="font-mono text-sm">EF.ARR</code>, record out of range) is reported as unverifiable. With <strong>Exact FCI</strong>, the optional <em>Ignore the EF.ARR reference in the exact FCI</em> checkbox compares the FCI without the <code class="font-mono text-sm">'8B'</code> value: a different record number no longer fails while every other DO is still compared byte-for-byte (the reference&rsquo;s meaning is verified by this check).</li>
<li><strong>File attributes</strong> — file type, size, record length and record count, taken from the FCP template (any may be left unset).</li>
<li><strong>Check contents</strong> (optional) — <strong>Exact</strong> hex equality, or <strong>Mask</strong> where <code class="font-mono text-sm">?</code> is a per-nibble wildcard (a mask with no <code class="font-mono text-sm">?</code> is a prefix match, e.g. <code class="font-mono text-sm">0891</code> for the IMSI MCC/MNC). Record files store a per-record list.</li>
</ul>
+86 -7
View File
@@ -1905,7 +1905,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.25.3';
const SIMPLE_VERSION = '3.25.4';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -18943,6 +18943,7 @@ function profilerValidateProfile(obj) {
const ar = r.accessRules;
if (typeof ar !== 'object' || !Array.isArray(ar.expected) ||
ar.expected.some(x => typeof x !== 'string')) return 'Invalid access rules: ' + r.path;
if (ar.mode !== undefined && !['exact', 'contains'].includes(ar.mode)) return 'Invalid access rules mode: ' + r.path;
}
}
return null;
@@ -19827,14 +19828,22 @@ function profilerRenderAccessRulesEditor(i, r) {
const lines = (ar && ar.expected) || [];
const on = !!(ar && ar.enabled !== false);
const open = on && !!_profilerArrOpen[i];
const contains = !!(ar && ar.mode === 'contains');
let html = '<div class="flex items-center justify-between mb-1 mt-1">';
if (on) {
html += '<button onclick="profilerArrToggleOpen(' + i + ')" class="text-xs text-gray-600 dark:text-slate-400 hover:text-gray-800 dark:hover:text-slate-200">' +
esc((open ? '\u25be ' : '\u25b8 ') + t('Check access rules') + (lines.length ? ' (' + lines.length + ')' : '')) + '</button>';
esc((open ? '\u25be ' : '\u25b8 ') + t('Check access rules') + (lines.length ? ' (' + lines.length + ')' : '') +
(contains ? ' \u00b7 ' + t('per operation') : '')) + '</button>';
} else {
html += '<span class="text-xs text-gray-600 dark:text-slate-400">' + esc(t('Check access rules')) + '</span>';
}
html += '<div class="flex items-center gap-3">';
if (on && lines.length) {
html += '<label class="text-xs flex items-center gap-1" title="' + esc(t('Exact: the card rules must equal the listed rules. Contains: each listed rule must be granted per operation; other card rules are ignored.')) + '">' +
esc(t('Match')) + ' <select onchange="profilerSetAccessRulesMode(' + i + ',this.value)" class="text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-0.5 dark:bg-slate-800">' +
'<option value="exact"' + (contains ? '' : ' selected') + '>' + esc(t('Exact')) + '</option>' +
'<option value="contains"' + (contains ? ' selected' : '') + '>' + esc(t('Contains (per operation)')) + '</option></select></label>';
}
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-ar-' + i + '" value="none"' + (!on ? ' checked' : '') + ' onchange="profilerSetAccessRules(' + i + ',false)"> ' + esc(t('None')) + '</label>';
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-ar-' + i + '" value="check"' + (on ? ' checked' : '') + ' onchange="profilerSetAccessRules(' + i + ',true)"> ' + esc(t('Check')) + '</label>';
html += '</div></div>';
@@ -20017,6 +20026,17 @@ function profilerSetAccessRulesIgnore(i, on) {
r.accessRules.ignoreFciRef = on;
}
// Exact (default) compares the canonical lines as stored; 'contains' checks
// every listed rule per operation and ignores the rules not listed (v3.25.4).
function profilerSetAccessRulesMode(i, v) {
if (!profilerDraft) return;
const r = profilerDraft.rules[i];
if (!r.accessRules) r.accessRules = { expected: [] };
if (v === 'contains') r.accessRules.mode = 'contains';
else delete r.accessRules.mode;
profilerRenderEditor();
}
function profilerRenderContentEditor(i, r) {
const c = r.content;
const mode = c ? c.mode : 'exact';
@@ -20423,10 +20443,14 @@ async function profilerRunRule(r, source) {
// FCI's '8B' reference points at - record-number independent, so cards
// with a different EF.ARR layout still verify.
if (r.accessRules && r.accessRules.enabled !== false && (r.accessRules.expected || []).length) {
// 'contains' (per operation, v3.25.4): every listed rule must be granted
// by the card - matched per AM bit / command entry, whatever AM grouping
// the card uses - and a rule not listed is not checked.
const arMode = r.accessRules.mode === 'contains' ? 'contains' : 'exact';
const ref = fcpArrRefFromFci(sel.fci_hex);
if (!ref) {
res.status = 'fail';
res.checks.push({ label: 'accessRules', expected: r.accessRules.expected, actual: null, ok: false,
res.checks.push({ label: 'accessRules', mode: arMode, expected: r.accessRules.expected, actual: null, ok: false,
missing: r.accessRules.expected.slice(), extra: [] });
} else {
let arrs = [];
@@ -20436,15 +20460,17 @@ async function profilerRunRule(r, source) {
const got = fcpArrRules(ref, arrs, r.path);
if (!got.found) {
if (res.status === 'pass') res.status = 'error';
res.checks.push({ label: 'accessRules', expected: r.accessRules.expected, actual: null, ok: false,
res.checks.push({ label: 'accessRules', mode: arMode, expected: r.accessRules.expected, actual: null, ok: false,
unresolved: true, missing: [], extra: [] });
} else {
const exp = r.accessRules.expected;
const missing = exp.filter(x => !got.rules.includes(x));
const extra = got.rules.filter(x => !exp.includes(x));
const missing = arMode === 'contains'
? exp.filter(x => !fcpArrRuleSatisfied(x, got.rules))
: exp.filter(x => !got.rules.includes(x));
const extra = arMode === 'contains' ? [] : got.rules.filter(x => !exp.includes(x));
const ok = !missing.length && !extra.length;
if (!ok) res.status = 'fail';
res.checks.push({ label: 'accessRules', expected: exp, actual: got.rules, ok: ok,
res.checks.push({ label: 'accessRules', mode: arMode, expected: exp, actual: got.rules, ok: ok,
missing: missing, extra: extra });
}
}
@@ -20879,6 +20905,50 @@ function fcpArrRuleEncode(rule) {
return am + (scs.length ? ': ' + scs.join(' AND ') : '');
}
// The AM entries a rule asserts, for the per-operation 'contains' match
// (v3.25.4): one per operation bit (bit form) or the command form
// (RESIZE/INCREASE); the proprietary (b8) form stays whole - its high nibble
// is opaque. Null for the forms outside the supported subset (raw).
function fcpArrRuleOps(m) {
if (!m || m.raw !== undefined) return null;
if (m.kind === 'bits') {
if (m.am & 0x80) return [{ am: m.am & 0xFF }];
const ops = [];
for (const b of FCP_ARR_AM_BITS) if (m.am & b[0]) ops.push({ bit: b[0] });
return ops;
}
return [{ kind: m.kind }];
}
// Does one card rule cover an operation of the expected rule?
function fcpArrRuleOpCovered(op, cm) {
if (!cm || cm.raw !== undefined) return false;
if (op.bit !== undefined) return cm.kind === 'bits' && (cm.am & op.bit) !== 0;
if (op.am !== undefined) return cm.kind === 'bits' && (cm.am & 0xFF) === op.am;
return cm.kind === op.kind;
}
// 'Contains' match (v3.25.4): every operation of the expected rule must be
// covered by a card rule with the same SC list, whatever AM grouping the card
// uses; a rule not listed is not checked. A form outside the supported
// subset (raw, or a card line with a label prefix) compares by exact string.
function fcpArrRuleSatisfied(line, cardRules) {
const rules = cardRules || [];
const m = fcpArrRuleParse(line);
if (m.raw !== undefined) return rules.includes(m.raw);
const ops = fcpArrRuleOps(m);
if (!ops || !ops.length) return rules.includes(line);
const want = (m.scs || []).map(fcpArrScEncode).join(' AND ');
for (const op of ops) {
if (!rules.some(x => {
const cm = fcpArrRuleParse(x);
return cm.raw === undefined && fcpArrRuleOpCovered(op, cm) &&
(cm.scs || []).map(fcpArrScEncode).join(' AND ') === want;
})) return false;
}
return true;
}
// The editor mutates the parsed model and re-encodes; a raw model is left as
// it is (its row is read-only).
function fcpArrRuleToggleBit(rule, bit, on) {
@@ -21209,6 +21279,10 @@ function profilerFciInput(i, value) {
function profilerAccessRulesDiffHtml(c) {
let html = '<div class="mt-1 pl-2">';
html += '<div class="text-xs text-red-600 font-mono">' + esc(t('Access rules')) + '</div>';
if (c.mode === 'contains') {
html += '<div class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">' +
esc(t('Compared per operation; other card rules are ignored')) + '</div>';
}
if (c.unresolved) {
html += '<div class="text-xs text-yellow-600 mt-0.5">' +
esc(t('the EF.ARR reference could not be resolved (no cached EF.ARR or the record is out of range)')) + '</div>';
@@ -23441,6 +23515,11 @@ const LANG_RU = {
'Check': 'Проверять',
'Access rules': 'Правила доступа',
'Check access rules': 'Проверять правила доступа',
'Match': 'Сопоставление',
'Contains (per operation)': 'Содержит (по операциям)',
'per operation': 'по операциям',
'Exact: the card rules must equal the listed rules. Contains: each listed rule must be granted per operation; other card rules are ignored.': 'Точное: правила карты должны совпадать со всеми перечисленными. Содержит: каждое перечисленное правило должно быть разрешено — сравнение по операциям; остальные правила карты игнорируются.',
'Compared per operation; other card rules are ignored': 'Сравнение по операциям; остальные правила карты игнорируются',
'Ignore the EF.ARR reference in the exact FCI': 'Игнорировать ссылку на EF.ARR в точном FCI',
'the FCI has no EF.ARR reference': 'в FCI нет ссылки на EF.ARR',
'the EF.ARR reference could not be resolved (no cached EF.ARR or the record is out of range)': 'ссылку на EF.ARR не удалось разрешить (нет кэша EF.ARR или запись вне диапазона)',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v427';
const CACHE = 'simple-v428';
const URLS = [
'index.html',
'help.html',
+106 -1
View File
@@ -22,7 +22,7 @@ function extractFunc(src, name, asyncFn) {
}
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'pysimCustomNormPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'profilerMatchedRecordsText', 'profilerCloneName', 'profilerClone', 'profilerNewId', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode',
'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerArrToggleOpen', 'profilerArrShiftOpen', 'profilerAccessRulesDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath',
'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleOps', 'fcpArrRuleOpCovered', 'fcpArrRuleSatisfied', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerArrToggleOpen', 'profilerArrShiftOpen', 'profilerAccessRulesDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath',
'pinKeyRefName', 'fcpPsTemplate', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel', 'pysimFsInfoHtml', 'profilerLabelText', 'profilerResultsHeaderText', 'profilerRenderResultsView', 'profilerBuildFileRuleFromSnapshot', 'profilerSnapshotPickListHtml', 'profilerScanSetTarget'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
@@ -1000,6 +1000,45 @@ test('profilerRunRule checks the access rules against the ARR copies', async ()
assert.ok(unres.checks.find(c => c.label === 'accessRules').unresolved);
});
test('the access-rules Contains mode checks the listed rules per operation', async () => {
const FCI2 = '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0';
const line = (am, scs) => fcpArrRuleEncode({ kind: 'bits', am: am, scs: scs });
const ALW = { kind: 'alw' }, ADM1 = { kind: 'key', ref: 0x0A, uq: 0x08 };
// the card's record: READ+WRITE combined always, the other bits ADM1, RESIZE never
const arrs = [{ fid: '2F06', path: 'MF/2F06',
records: ['FFFF', '800105900080011AA40683010A9501088401D49700'] }];
const src = {
select: async () => ({ name: 'EF.SAMPLE', file_type: 'linear_fixed', file_size: null,
record_len: 6, num_of_rec: 2, fci_hex: FCI2, exists: true }),
read: async () => ({ success: false }),
arrs: () => arrs,
};
const rule = ar => ({ type: 'file', path: 'MF/7F20/6F07', name: 'EF.SAMPLE',
fileType: 'linear_fixed', fciMode: 'type_size', fciHex: FCI2, accessRules: ar });
// READ/WRITE always, RESIZE not listed: the combined 0x05 satisfies both
// lines; the ADM1 and RESIZE rules are not checked
const ok = await profilerRunRule(rule({ mode: 'contains', expected: [line(0x01, [ALW]), line(0x04, [ALW])] }), src);
assert.strictEqual(ok.status, 'pass', JSON.stringify(ok.checks));
assert.strictEqual(ok.checks.find(c => c.label === 'accessRules').mode, 'contains');
// the same lines in the default exact mode fail on the AM grouping
const ex = await profilerRunRule(rule({ expected: [line(0x01, [ALW]), line(0x04, [ALW])] }), src);
assert.strictEqual(ex.status, 'fail');
assert.strictEqual(ex.checks.find(c => c.label === 'accessRules').missing.length, 2);
// a listed operation the card grants with another condition fails
const miss = await profilerRunRule(rule({ mode: 'contains', expected: [line(0x02, [ALW])] }), src);
assert.strictEqual(miss.status, 'fail');
const mc = miss.checks.find(c => c.label === 'accessRules');
assert.deepStrictEqual(mc.missing, [line(0x02, [ALW])]);
assert.deepStrictEqual(mc.extra, []);
// the RESIZE condition is checked when listed
const resize = fcpArrRuleEncode({ kind: 'resize', scs: [{ kind: 'nev' }] });
const rOk = await profilerRunRule(rule({ mode: 'contains', expected: [resize] }), src);
assert.strictEqual(rOk.status, 'pass', JSON.stringify(rOk.checks));
const rBad = await profilerRunRule(rule({ mode: 'contains',
expected: [fcpArrRuleEncode({ kind: 'resize', scs: [ADM1] })] }), src);
assert.strictEqual(rBad.status, 'fail');
});
test('ignoreFciRef compares the FCI without the EF.ARR reference', async () => {
const FCI2 = '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0';
const FCI9 = '621A82054221000F0583026F4F8A01058B032F06098802004B8801B0';
@@ -1108,6 +1147,30 @@ test('fcpArrRule mutators re-encode the canonical line', () => {
assert.strictEqual(fcpArrRuleEncode(fcpArrRuleToggleBit(raw, 0x01, true)), 'command description 0x85 (D4)');
});
test('fcpArrRuleSatisfied matches per operation, whatever the AM grouping', () => {
const line = (am, scs) => fcpArrRuleEncode({ kind: 'bits', am: am, scs: scs });
const ALW = { kind: 'alw' }, ADM1 = { kind: 'key', ref: 0x0A, uq: 0x08 };
// the card combines READ+WRITE in one AM byte and grants the other bits ADM1
const card = [line(0x05, [ALW]), line(0x1A, [ADM1])];
assert.ok(fcpArrRuleSatisfied(line(0x01, [ALW]), card)); // READ covered by the combined 0x05
assert.ok(fcpArrRuleSatisfied(line(0x04, [ALW]), card)); // WRITE too
assert.ok(fcpArrRuleSatisfied(line(0x1A, [ADM1]), card)); // a multi-bit rule, same grouping
assert.ok(!fcpArrRuleSatisfied(line(0x02, [ALW]), card)); // UPDATE is ADM1, not always
assert.ok(!fcpArrRuleSatisfied(line(0x08, [ALW]), card)); // DEACTIVATE is ADM1
// a command entry keys by command, not by bits
const resize = fcpArrRuleEncode({ kind: 'resize', scs: [{ kind: 'nev' }] });
assert.ok(fcpArrRuleSatisfied(resize, card.concat([resize])));
assert.ok(!fcpArrRuleSatisfied(fcpArrRuleEncode({ kind: 'resize', scs: [ADM1] }), card.concat([resize])));
assert.ok(!fcpArrRuleSatisfied(resize, card));
// a form outside the subset compares by exact string
assert.ok(fcpArrRuleSatisfied('command description 0x85 (D4)', ['command description 0x85 (D4)']));
assert.ok(!fcpArrRuleSatisfied('command description 0x85 (D4)', card));
// the proprietary (b8) form stays whole
const prop = fcpArrRuleEncode({ kind: 'bits', am: 0x81, scs: [ALW] });
assert.ok(fcpArrRuleSatisfied(prop, [prop]));
assert.ok(!fcpArrRuleSatisfied(prop, [line(0x01, [ALW])]));
});
test('the access-rules editor renders structured rows', () => {
global.t = s => s;
const r = { fciMode: 'exact', accessRules: { expected: [
@@ -1159,6 +1222,36 @@ test('the access-rules editor renders structured rows', () => {
delete global.t;
});
test('the access-rules Match selector switches to per-operation Contains', () => {
global.t = s => s;
const r = { fciMode: 'type_size', accessRules: { expected: [
'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always'] } };
_profilerArrOpen = { 3: true };
const exact = profilerRenderAccessRulesEditor(3, r);
assert.ok(exact.includes('profilerSetAccessRulesMode(3,this.value)'), exact);
assert.ok(exact.includes('<option value="exact" selected>Exact</option>'), exact);
assert.ok(exact.includes('<option value="contains">Contains (per operation)</option>'), exact);
assert.ok(!exact.includes('\u00b7 per operation'), exact);
const contains = profilerRenderAccessRulesEditor(3, { ...r,
accessRules: { ...r.accessRules, mode: 'contains' } });
assert.ok(contains.includes('<option value="contains" selected>Contains (per operation)</option>'), contains);
assert.ok(contains.includes('Check access rules (1) \u00b7 per operation'), contains);
// the collapsed header carries the mode badge too
_profilerArrOpen = {};
const closed = profilerRenderAccessRulesEditor(3, { ...r,
accessRules: { ...r.accessRules, mode: 'contains' } });
assert.ok(closed.includes('\u00b7 per operation'), closed);
// None hides the selector, an empty block has none
const none = profilerRenderAccessRulesEditor(3, { ...r,
accessRules: { ...r.accessRules, enabled: false } });
assert.ok(!none.includes('profilerSetAccessRulesMode'), none);
_profilerArrOpen = { 4: true };
const empty = profilerRenderAccessRulesEditor(4, { fciMode: 'type_size', accessRules: { expected: [] } });
assert.ok(!empty.includes('profilerSetAccessRulesMode'), empty);
_profilerArrOpen = {};
delete global.t;
});
test('the access-rule open state survives rule deletion', () => {
assert.deepStrictEqual(profilerArrShiftOpen({ 0: true, 2: true, 3: true }, 1), { 0: true, 1: true, 2: true });
assert.deepStrictEqual(profilerArrShiftOpen({ 0: true, 2: true, 3: true }, 2), { 0: true, 2: true });
@@ -1186,6 +1279,12 @@ test('profile validation accepts the access-rules field', () => {
accessRules: { expected: 'nope' } }] }));
assert.ok(profilerValidateProfile({ ...base, rules: [{ type: 'file', path: 'MF/7F20/6F07',
accessRules: { expected: [1] } }] }));
assert.strictEqual(profilerValidateProfile({ ...base, rules: [{ type: 'file', path: 'MF/7F20/6F07',
accessRules: { expected: ['AM 0x01: always'], mode: 'exact' } }] }), null);
assert.strictEqual(profilerValidateProfile({ ...base, rules: [{ type: 'file', path: 'MF/7F20/6F07',
accessRules: { expected: ['AM 0x01: always'], mode: 'contains' } }] }), null);
assert.ok(profilerValidateProfile({ ...base, rules: [{ type: 'file', path: 'MF/7F20/6F07',
accessRules: { expected: ['AM 0x01: always'], mode: 'partial' } }] }));
});
test('the access-rules report block marks missing and extra lines', () => {
@@ -1201,6 +1300,12 @@ test('the access-rules report block marks missing and extra lines', () => {
const noref = profilerAccessRulesDiffHtml({ label: 'accessRules', ok: false,
expected: ['A'], actual: null, missing: ['A'], extra: [] });
assert.ok(noref.includes('no EF.ARR reference'), noref);
// contains: the mode note, no extra lines
const cont = profilerAccessRulesDiffHtml({ label: 'accessRules', mode: 'contains', ok: false,
expected: ['A'], actual: ['B'], missing: ['A'], extra: [] });
assert.ok(cont.includes('Compared per operation; other card rules are ignored'), cont);
assert.ok(cont.includes('\u2212 A'), cont);
assert.ok(!cont.includes('+ B'), cont);
delete global.t;
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.25.3"
version = "3.25.4"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.25.3'
VERSION = '3.25.4'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE