crossCheckAuthorization function
Runs the six checks over the Blueprint in yamlText with the role model
in configYaml.
Implementation
CrossCheckReport crossCheckAuthorization(String yamlText, String configYaml) {
final d = sbpRoot(yamlText);
final report = CrossCheckReport();
// --- the registries ------------------------------------------------------
final ops = <String, Operation>{};
entriesOf(at(d, ['INDM', 'SVOPR', 'SVOPE-OPER-LST'])).forEach((_, v) {
final content = child(v, 'content');
final name = field(content, 'operationName');
final pre = field(content, 'content');
ops[name] = Operation(
name,
Access.of(child(v, 'AZREQ')),
_line(pre, 'Called by'),
_line(pre, 'Safeguards'),
pre,
);
});
final opNames = ops.keys.toSet();
final screens = <String, Screen>{};
entriesOf(at(d, ['XID', 'XCS', 'SCRDZ', 'SCRINV', 'SCREN-ITEM-LST'])).forEach(
(k, v) {
final actions = <String, Set<String>>{};
entriesOf(at(v, ['SCAC', 'SCRAC-ITEM-LST'])).forEach((ak, av) {
final description = field(child(av, 'content'), 'description');
actions[ak] = {
for (final m in _word.allMatches(description))
if (opNames.contains(m.group(1))) m.group(1)!,
};
});
final name = k.replaceFirst('SCREN-ITEM-', '');
screens[name] = Screen(name, Access.of(child(v, 'AZREQ')), actions);
},
);
final useCases = <String, UseCase>{};
entriesOf(at(d, ['TOMC', 'PSAAI', 'INCA', 'INEN-INTE-LST'])).forEach((k, v) {
final all = <String>{};
_serverOperations(v, all);
final steps = <Step>[];
entriesOf(at(v, ['MASUSC', 'MNSST-STEP-LST'])).forEach((sk, st) {
final c = child(st, 'content');
final op = field(c, 'serverOperation');
if (op.isNotEmpty) {
steps.add(
Step(
op,
'${_raw(c, 'actorAction')} // ${_raw(c, 'systemResponse')}',
sk,
),
);
}
});
entriesOf(at(v, ['USCAEX', 'EXTEN-EXTE-LST'])).forEach((_, ev) {
entriesOf(child(ev, 'EXTST-STEP-LST')).forEach((sk, st) {
final c = child(st, 'content');
final op = field(c, 'serverOperation');
if (op.isNotEmpty) {
steps.add(
Step(op, '${_raw(c, 'action')} // ${_raw(c, 'response')}', sk),
);
}
});
});
final name = k.replaceFirst('INEN-INTE-', '');
useCases[name] = UseCase(
name,
field(child(v, 'INID'), 'primaryActor'),
all,
steps,
);
});
report
..operations = ops.length
..screens = screens.length
..useCases = useCases.length;
final allKeys = <String>{
for (final o in ops.values) ...o.access.keys,
for (final s in screens.values) ...s.access.keys,
};
final model = RoleModel.parse(configYaml, allKeys);
void finding(String check, String message) =>
report.findings[check]!.add(message);
// --- A operations --------------------------------------------------------
final svopr = textOf(child(at(d, ['INDM', 'SVOPR']), 'content'));
final handlerTable = svopr.contains('**Custom authorization handlers')
? svopr
.split('**Custom authorization handlers')[1]
.split('**Operations per area')
.first
: '';
final handlers = [
for (final m in RegExp(
r'^\| `(\w+)` \|',
multiLine: true,
).allMatches(handlerTable))
m.group(1)!,
];
ops.forEach((name, o) {
if (model.reach(o.access).isEmpty) {
finding(
'A operations reachable',
'$name: no role reaches it (${o.access.describe()})',
);
}
final h = o.access.handler;
if (h.isNotEmpty && !handlers.contains(h)) {
finding(
'A operations reachable',
'$name: handler $h not in the SVOPR handler table',
);
}
});
for (final h in handlers) {
if (!ops.values.any((o) => o.access.handler == h)) {
finding(
'A operations reachable',
'handler $h in the table but used by no operation',
);
}
}
// --- B screens -----------------------------------------------------------
screens.forEach((s, screen) {
final sr = model.reach(screen.access);
screen.actions.forEach((a, actionOps) {
for (final o in actionOps) {
if (sr.intersection(model.reach(ops[o]!.access)).isEmpty) {
finding(
'B screen actions',
'$s $a: $o (${ops[o]!.access.describe()}) not reachable by any role admitted to the screen (${screen.access.kind})',
);
}
}
});
});
// --- C use cases ---------------------------------------------------------
// Only meaningful when questions are pending: an empty list would match
// every `BQ-` citation.
final pendingPattern = RegExp(
r'BQ-(' +
model.pendingQuestions.map((q) => q.substring(3)).join('|') +
r')\b',
);
final otherPattern = RegExp(
r'\b(administrator|approver|service desk|service-desk|agent|job|scheduler|former|owner of the tenant|platform)\b',
caseSensitive: false,
);
final selfPattern = RegExp(
r'^\s*The (application owner|user|end user|person)\b',
);
useCases.forEach((u, uc) {
final actorRoles = model.actorRoles[uc.actor.trim()] ?? <String>{};
for (final step in uc.steps) {
final o = ops[step.operation];
if (o == null) {
finding(
'C use cases',
'$u: step names unknown operation ${step.operation}',
);
continue;
}
if (actorRoles.contains('system') ||
actorRoles.intersection(model.reach(o.access)).isNotEmpty ||
model.otherActorOperations.contains(step.operation)) {
continue;
}
final act = step.actorText.split(' // ').first;
final short = act.trim();
if (model.pendingQuestions.isNotEmpty && pendingPattern.hasMatch(act)) {
report.pending.add(
'$u ${step.id}: ${step.operation} — waits for an open question: '
'"${short.length > 90 ? short.substring(0, 90) : short}"',
);
continue;
}
if (otherPattern.hasMatch(act) && !selfPattern.hasMatch(act)) {
report.otherActor.add(
'$u ${step.id}: ${step.operation} — by another actor: '
'"${short.length > 90 ? short.substring(0, 90) : short}"',
);
continue;
}
finding(
'C use cases',
'$u ${step.id} (${uc.actor}): ${step.operation} needs ${o.access.describe()} — '
'"${short.length > 100 ? short.substring(0, 100) : short}"',
);
}
});
// --- D step-up -----------------------------------------------------------
final suap = textOf(
child(
at(d, ['SAAM', 'ACCM', 'IDAUT', 'AUTHEN', 'AUFL', 'SUAP']),
'content',
),
);
final suapList = suap.contains('| Default step-up')
? suap.split('| Default step-up')[1].split('\n\n').first
: '';
final suapOps = {
for (final m in _backticked.allMatches(suapList))
if (ops.containsKey(m.group(1))) m.group(1)!,
};
for (final o in suapOps.toList()..sort()) {
if (!('${ops[o]!.safeguards}${ops[o]!.preamble}').toLowerCase().contains(
'step-up',
)) {
finding(
'D step-up',
'$o: in the SUAP default list but its preamble names no step-up',
);
}
}
ops.forEach((name, o) {
if (RegExp('step-up', caseSensitive: false).hasMatch(o.safeguards) &&
!suapOps.contains(name) &&
!o.safeguards.contains('tenant lists')) {
finding(
'D step-up',
'$name: safeguards name step-up but SUAP does not list it',
);
}
});
// --- E four-eyes ---------------------------------------------------------
final roma = textOf(child(at(d, ['SAAM', 'ACCM', 'ROMA']), 'content'));
if (roma.contains('**Step-up and four-eyes by default.**')) {
final para = roma
.split('**Step-up and four-eyes by default.**')[1]
.split('**Small installations.**')
.first;
final part = para.contains('Four-eyes, where the tenant has it on')
? para
.split('Four-eyes, where the tenant has it on')[1]
.split('Two platform administrators')
.first
: '';
final feOps = {
for (final m in _backticked.allMatches(part))
if (ops.containsKey(m.group(1))) m.group(1)!,
};
for (final o in feOps.toList()..sort()) {
if (!('${ops[o]!.safeguards}${ops[o]!.preamble}').toLowerCase().contains(
'four-eyes',
)) {
finding(
'E four-eyes',
'$o: in the ROMA four-eyes default list but its preamble names no four-eyes',
);
}
}
}
// --- F called by ---------------------------------------------------------
ops.forEach((name, o) {
final cb = o.calledBy;
final registeredScreens = {
for (final m in RegExp(r'`([A-Z][A-Z0-9\-]+)`').allMatches(cb))
m.group(1)!,
};
final registeredUseCases = {
for (final m in RegExp(r'UC-\d+').allMatches(cb)) m.group(0)!,
};
final actingScreens = {
for (final e in screens.entries)
if (e.value.actions.values.any((s) => s.contains(name))) e.key,
};
final namingUseCases = {
for (final e in useCases.entries)
if (e.value.operations.contains(name)) e.key,
};
for (final s
in actingScreens.difference(registeredScreens).toList()..sort()) {
finding('F called by', '$name: screen $s calls it, "Called by" lacks it');
}
for (final s
in registeredScreens.difference(actingScreens).toList()..sort()) {
if (screens.containsKey(s)) {
finding(
'F called by',
'$name: "Called by" names $s, whose actions do not call it',
);
}
}
for (final u
in namingUseCases.difference(registeredUseCases).toList()..sort()) {
finding(
'F called by',
'$name: $u names it in a step, "Called by" lacks it',
);
}
if (cb.isEmpty) finding('F called by', '$name: no "Called by" line');
if (RegExp(r'sbpa\d+|no screen yet').hasMatch(cb)) {
finding(
'F called by',
'$name: "Called by" still says ${cb.length > 80 ? cb.substring(0, 80) : cb}',
);
}
});
return report;
}