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:
@@ -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> — проверяет условия доступа файла как <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 по умолчанию пропускается — значимая проверка это правила); в редакторе захваченные правила показываются структурированными строками — операция в виде флажков битов (или командной формы 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> — проверяет условия доступа файла как <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 по умолчанию пропускается — значимая проверка это правила); в редакторе захваченные правила показываются структурированными строками — операция в виде флажков битов (или командной формы 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
@@ -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> — verifies a file’s access conditions as <em>decoded rules</em>, independent of which <code class="font-mono text-sm">EF.ARR</code> record the FCI’s security-attribute reference (<code class="font-mono text-sm">'8B'</code>) points at: the referenced record’s rules are resolved from the card’s (or the snapshot’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’s own contents are skipped by default — the rules are the meaningful check); the editor shows the captured rules as structured rows — 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’s meaning is verified by this check).</li>
|
||||
<li><strong>Access rules</strong> — verifies a file’s access conditions as <em>decoded rules</em>, independent of which <code class="font-mono text-sm">EF.ARR</code> record the FCI’s security-attribute reference (<code class="font-mono text-sm">'8B'</code>) points at: the referenced record’s rules are resolved from the card’s (or the snapshot’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’s own contents are skipped by default — the rules are the meaningful check); the editor shows the captured rules as structured rows — 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’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’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
@@ -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
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v427';
|
||||
const CACHE = 'simple-v428';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user