crossCheckAuthorization function

CrossCheckReport crossCheckAuthorization(
  1. String yamlText,
  2. String configYaml
)

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;
}