/ src / archan / plugins / checkers.py
checkers.py
  1  """Checker module."""
  2  
  3  from __future__ import annotations
  4  
  5  from typing import TYPE_CHECKING, Any
  6  
  7  from archan import Argument, Checker
  8  from archan.errors import DesignStructureMatrixError
  9  from archan.logging import Logger
 10  
 11  if TYPE_CHECKING:
 12      from archan.dsm import DesignStructureMatrix, DomainMappingMatrix, MultipleDomainMatrix
 13  
 14  logger = Logger.get_logger(__name__)
 15  
 16  
 17  class CompleteMediation(Checker):
 18      """Complete mediation check."""
 19  
 20      identifier = "archan.CompleteMediation"
 21      name = "Complete Mediation"
 22      description = """
 23      Every access to every object must be checked for authority.
 24      This principle, when systematically applied, is the primary underpinning
 25      of the protection system. It forces a system-wide view of access control,
 26      which in addition to normal operation includes initialization, recovery,
 27      shutdown, and maintenance. It implies that a foolproof method of
 28      identifying the source of every request must be devised. It also requires
 29      that proposals to gain performance by remembering the result of an
 30      authority check be examined skeptically. If a change in authority occurs,
 31      such remembered results must be systematically updated."""
 32      hint = "Remove the dependencies or deviate them through a broker module."
 33  
 34      @staticmethod
 35      def generate_mediation_matrix(
 36          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
 37      ) -> list[list[int]]:
 38          """Generate the mediation matrix of the given matrix.
 39  
 40          Rules for mediation matrix generation:
 41  
 42          - Set `-1` for items **NOT** to be considered
 43          - Set `0` for items which **MUST NOT** be present
 44          - Set `1` for items which **MUST** be present
 45  
 46          Each module has optional dependencies to itself.
 47  
 48          - Framework has optional dependency to all framework items (-1),
 49            and to nothing else.
 50          - Core libraries have dependencies to framework.
 51            Dependencies to other core libraries are tolerated.
 52          - Application libraries have dependencies to framework.
 53            Dependencies to other core or application libraries are tolerated.
 54            No dependencies to application modules.
 55          - Application modules have dependencies to framework and libraries.
 56            Dependencies to other application modules
 57            should be mediated over a broker.
 58            Dependencies to data are tolerated.
 59          - Data have no dependencies at all
 60            (but framework/libraries would be tolerated).
 61  
 62          Parameters:
 63              dsm: The DSM to generate the mediation matrix for.
 64  
 65          Raises:
 66              DesignStructureMatrixError: The mediation matrix could not be generated.
 67  
 68          Returns:
 69              The mediation matrix.
 70          """
 71          cat = dsm.categories
 72          ent = dsm.entities
 73          size = dsm.size[0]
 74  
 75          if not cat:
 76              cat = ["appmodule"] * size
 77  
 78          packages = [entity.split(".")[0] for entity in ent]
 79  
 80          # define and initialize the mediation matrix
 81          mediation_matrix = [[0 for _ in range(size)] for _ in range(size)]
 82  
 83          for i in range(size):
 84              for j in range(size):
 85                  if cat[i] == "framework":
 86                      if cat[j] == "framework":
 87                          mediation_matrix[i][j] = -1
 88                      else:
 89                          mediation_matrix[i][j] = 0
 90                  elif cat[i] == "corelib":
 91                      if cat[j] in {"framework", "corelib"} or ent[i].startswith(packages[j] + ".") or i == j:
 92                          mediation_matrix[i][j] = -1
 93                      else:
 94                          mediation_matrix[i][j] = 0
 95                  elif cat[i] == "applib":
 96                      if cat[j] in {"framework", "corelib", "applib"} or ent[i].startswith(packages[j] + ".") or i == j:
 97                          mediation_matrix[i][j] = -1
 98                      else:
 99                          mediation_matrix[i][j] = 0
100                  elif cat[i] == "appmodule":
101                      # we cannot force an app module to import things from
102                      # the broker if the broker itself did not import anything
103                      ignore = (
104                          cat[j] in {"framework", "corelib", "applib", "broker", "data"}
105                          or ent[i].startswith(packages[j] + ".")
106                          or i == j
107                      )
108                      if ignore:
109                          mediation_matrix[i][j] = -1
110                      else:
111                          mediation_matrix[i][j] = 0
112                  elif cat[i] == "broker":
113                      # we cannot force the broker to import things from
114                      # app modules if there is nothing to be imported.
115                      # also broker should be authorized to use third apps
116                      ignore = (
117                          cat[j] in {"appmodule", "corelib", "framework"}
118                          or ent[i].startswith(packages[j] + ".")
119                          or i == j
120                      )
121                      if ignore:
122                          mediation_matrix[i][j] = -1
123                      else:
124                          mediation_matrix[i][j] = 0
125                  elif cat[i] == "data":
126                      if cat[j] == "framework" or i == j:
127                          mediation_matrix[i][j] = -1
128                      else:
129                          mediation_matrix[i][j] = 0
130                  else:
131                      # mediation_matrix[i][j] = -2  # errors in the generation
132                      raise DesignStructureMatrixError(f"Mediation matrix value NOT generated for {i}:{j}")
133  
134          return mediation_matrix
135  
136      @staticmethod
137      def matrices_compliance(
138          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
139          complete_mediation_matrix: list[list[int]],
140      ) -> tuple[bool, str]:
141          """Check if matrix and its mediation matrix are compliant.
142  
143          Parameters:
144              dsm: The DSM to check.
145              complete_mediation_matrix: 2-dim array.
146  
147          Raises:
148              DesignStructureMatrixError: When the matrices are not compliant.
149  
150          Returns:
151              True if compliant, else False.
152          """
153          matrix = dsm.data
154          rows_dep_matrix = len(matrix)
155          cols_dep_matrix = len(matrix[0])
156          rows_med_matrix = len(complete_mediation_matrix)
157          cols_med_matrix = len(complete_mediation_matrix[0])
158  
159          if rows_dep_matrix != rows_med_matrix or cols_dep_matrix != cols_med_matrix:
160              raise DesignStructureMatrixError("Matrices are NOT compliant (number of rows/columns not equal)")
161  
162          discrepancy_found = False
163          messages = []
164          for i in range(rows_dep_matrix):
165              for j in range(cols_dep_matrix):
166                  discrepancy = (complete_mediation_matrix[i][j] == 0 and matrix[i][j] > 0) or (
167                      complete_mediation_matrix[i][j] == 1 and matrix[i][j] < 1
168                  )
169                  if discrepancy:
170                      discrepancy_found = True
171                      messages.append(
172                          f"Untolerated dependency at {i}:{j} ({dsm.entities[i]}:{dsm.entities[j]}): "
173                          f"{matrix[i][j]} instead of {complete_mediation_matrix[i][j]}",
174                      )
175  
176          message = "\n".join(messages)
177  
178          return not discrepancy_found, message
179  
180      def check(
181          self,
182          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
183          **kwargs: Any,  # noqa: ARG002
184      ) -> tuple[Any, str]:
185          """Check if matrix and its mediation matrix are compliant.
186  
187          It means that number of dependencies for each (line, column) is either
188          0 if the mediation matrix (line, column) is 0, or >0 if the mediation
189          matrix (line, column) is 1.
190  
191          Parameters:
192              dsm: The DSM to check.
193              **kwargs: Optional additional keyword arguments.
194  
195          Returns:
196              True if compliant, else False.
197          """
198          # generate complete_mediation_matrix according to each category
199          med_matrix = CompleteMediation.generate_mediation_matrix(dsm)
200          return CompleteMediation.matrices_compliance(dsm, med_matrix)
201  
202  
203  class EconomyOfMechanism(Checker):
204      """Economy of mechanism check."""
205  
206      identifier = "archan.EconomyOfMechanism"
207      name = "Economy of Mechanism"
208      hint = "Reduce the number of dependencies in your own code or increase the simplicity factor."
209      description = """
210      Keep the design as simple and small as possible. This well-known principle
211      applies to any aspect of a system, but it deserves emphasis for protection
212      mechanisms for this reason: design and implementation errors that result in
213      unwanted access paths will not be noticed during normal use (since normal
214      use usually does not include attempts to exercise improper access paths).
215      As a result, techniques such as line-by-line inspection of software and
216      physical examination of hardware that implements protection mechanisms are
217      necessary. For such techniques to be successful, a small and simple design
218      is essential."""
219  
220      argument_list = (
221          Argument(
222              "simplicity_factor",
223              int,
224              "If the number of cells with dependencies in the DSM "
225              "is lower than the DSM size multiplied by the simplicity "
226              "factor, then this criterion is verified.",
227              2,
228          ),
229      )
230  
231      def check(
232          self,
233          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
234          simplicity_factor: int = 2,
235          **kwargs: Any,  # noqa: ARG002
236      ) -> tuple[Any, str]:
237          """Check economy of mechanism.
238  
239          As first abstraction, number of dependencies between two modules
240          < 2 * the number of modules
241          (dependencies to the framework are NOT considered).
242  
243          Parameters:
244              dsm: The DSM to check.
245              simplicity_factor: Simplicity factor.
246              **kwargs: Optional additional keyword arguments.
247  
248          Returns:
249              True if economic, else False.
250          """
251          # economy_of_mechanism
252          economy_of_mechanism = False
253          message = ""
254          data = dsm.data
255          categories = dsm.categories
256          dsm_size = dsm.size[0]
257  
258          if not categories:
259              categories = ["appmodule"] * dsm_size
260  
261          dependency_number = 0
262          # evaluate Matrix(data)
263          for i in range(dsm_size):
264              for j in range(dsm_size):
265                  count_dependency = (
266                      categories[i] not in {"framework", "corelib"}
267                      and categories[j] not in {"framework", "corelib"}
268                      and data[i][j] > 0
269                  )
270                  if count_dependency:
271                      dependency_number += 1
272                      # check comparison result
273          if dependency_number < dsm_size * simplicity_factor:
274              economy_of_mechanism = True
275          else:
276              message = " ".join(
277                  [
278                      f"Number of dependencies ({dependency_number})",
279                      f"> number of rows ({dsm_size})",
280                      f"* simplicity factor ({simplicity_factor}) = {dsm_size * simplicity_factor}",
281                  ],
282              )
283          return economy_of_mechanism, message
284  
285  
286  class SeparationOfPrivileges(Checker):
287      """Separation of privileges check."""
288  
289      identifier = "archan.SeparationOfPrivileges"
290      name = "Separation of Privileges"
291      description = """
292      Where feasible, a protection mechanism that requires two keys to unlock it
293      is more robust and flexible than one that allows access to the presenter of
294      only a single key. The relevance of this observation to computer systems
295      was pointed out by R. Needham in 1973. The reason is that, once the
296      mechanism is locked, the two keys can be physically separated and distinct
297      programs, organizations, or individuals made responsible for them. From
298      then on, no single accident, deception, or breach of trust is sufficient to
299      compromise the protected information. This principle is often used in bank
300      safe-deposit boxes. It is also at work in the defense system that fires a
301      nuclear weapon only if two different people both give the correct command.
302      In a computer system, separated keys apply to any situation in which two or
303      more conditions must be met before access should be permitted. For example,
304      systems providing user-extendible protected data types usually depend on
305      separation of privilege for their implementation."""
306      # TODO: add hint
307  
308      def check(
309          self,
310          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
311          **kwargs: Any,
312      ) -> tuple[Any, str]:
313          """TODO: To implement."""
314          raise NotImplementedError
315  
316  
317  class LeastPrivileges(Checker):
318      """Least privileges check."""
319  
320      identifier = "archan.LeastPrivileges"
321      name = "Least Privileges"
322      description = """
323      Every program and every user of the system should operate using the least
324      set of privileges necessary to complete the job. Primarily, this principle
325      limits the damage that can result from an accident or error. It also
326      reduces the number of potential interactions among privileged programs to
327      the minimum for correct operation, so that unintentional, unwanted, or
328      improper uses of privilege are less likely to occur. Thus, if a question
329      arises related to misuse of a privilege, the number of programs that must
330      be audited is minimized. Put another way, if a mechanism can provide
331      "firewalls," the principle of least privilege provides a rationale for
332      where to install the firewalls. The military security rule of
333      "need-to-know" is an example of this principle."""
334      # TODO: add hint
335  
336      def check(
337          self,
338          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
339          **kwargs: Any,
340      ) -> tuple[Any, str]:
341          """TODO: To implement."""
342          raise NotImplementedError
343  
344  
345  class LeastCommonMechanism(Checker):
346      """Least common mechanism check."""
347  
348      identifier = "archan.LeastCommonMechanism"
349      name = "Least Common Mechanism"
350      hint = "Reduce number of modules having dependencies to the listed module."
351      description = """
352      Minimize the amount of mechanism common to more than one user and depended
353      on by all users. Every shared mechanism (especially one involving shared
354      variables) represents a potential information path between users and must
355      be designed with great care to be sure it does not unintentionally
356      compromise security. Further, any mechanism serving all users must be
357      certified to the satisfaction of every user, a job presumably harder than
358      satisfying only one or a few users. For example, given the choice of
359      implementing a new function as a supervisor procedure shared by all users
360      or as a library procedure that can be handled as though it were the user's
361      own, choose the latter course. Then, if one or a few users are not
362      satisfied with the level of certification of the function, they can provide
363      a substitute or not use it at all. Either way, they can avoid being harmed
364      by a mistake in it."""
365  
366      argument_list = (
367          Argument(
368              "independence_factor",
369              int,
370              "If the maximum dependencies for one module is inferior or "
371              "equal to the DSM size divided by the independence factor, "
372              "then this criterion is verified.",
373              5,
374          ),
375      )
376  
377      def check(
378          self,
379          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
380          independence_factor: int = 5,
381          **kwargs: Any,  # noqa: ARG002
382      ) -> tuple[Any, str]:
383          """Check least common mechanism.
384  
385          Parameters:
386              dsm: The DSM to check.
387              independence_factor: If the maximum dependencies for one
388                  module is inferior or equal to the DSM size divided by the
389                  independence factor, then this criterion is verified.
390              **kwargs: Optional additional keyword arguments.
391  
392          Returns:
393              True if least common mechanism, else False.
394          """
395          # leastCommonMechanismMatrix
396          least_common_mechanism = False
397          message = ""
398          # get the list of dependent modules for each module
399          data = dsm.data
400          categories = dsm.categories
401          dsm_size = dsm.size[0]
402  
403          if not categories:
404              categories = ["appmodule"] * dsm_size
405  
406          dependent_module_number = []
407          # evaluate Matrix(data)
408          for j in range(dsm_size):
409              dependent_module_number.append(0)
410              for i in range(dsm_size):
411                  if categories[i] != "framework" and categories[j] != "framework" and data[i][j] > 0:
412                      dependent_module_number[j] += 1
413          # except for the broker if any  and libs, check that threshold is not
414          # overlapped
415          #  index of brokers
416          #  and app_libs are set to 0
417          for index, item in enumerate(dsm.categories):
418              if item in {"broker", "applib"}:
419                  dependent_module_number[index] = 0
420          if max(dependent_module_number) <= dsm_size / independence_factor:
421              least_common_mechanism = True
422          else:
423              maximum = max(dependent_module_number)
424              module = dsm.entities[dependent_module_number.index(maximum)]
425              message = (
426                  f"Dependencies to {module} ({maximum}) > matrix size ({dsm_size}) / "
427                  "independence factor ({independence_factor}) = {dsm_size / independence_factor"
428              )
429  
430          return least_common_mechanism, message
431  
432  
433  class LayeredArchitecture(Checker):
434      """Layered architecture check.
435  
436      Check that the DSM can be diagonalized (no marks in upper right or
437      lower left corner).
438      """
439  
440      identifier = "archan.LayeredArchitecture"
441      name = "Layered Architecture"
442      description = """
443      The modules that are part of the project should be organized in a layered
444      way by means of groups. Modules like frameworks and librairies should be
445      put first, then other components like the main features of the project.
446      Security and data modules should be put last. A well layered architecture
447      should be visible in the form of a matrix which has dependencies into only
448      one corner (for example: in the lower-left part)."""
449      hint = "Ensure that your applications are listed in the right order when building the DSM, or remove dependencies."
450  
451      def check(
452          self,
453          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
454          **kwargs: Any,  # noqa: ARG002
455      ) -> tuple[Any, str]:
456          """Check layered architecture.
457  
458          Parameters:
459              dsm: The DSM to check.
460              **kwargs: Optional additional keyword arguments.
461  
462          Returns:
463              True if layered architecture else False, messages.
464          """
465          layered_architecture = True
466          messages = []
467          categories = dsm.categories
468          dsm_size = dsm.size[0]
469  
470          if not categories:
471              categories = ["appmodule"] * dsm_size
472  
473          for i in range(dsm_size - 1):
474              for j in range(i + 1, dsm_size):
475                  check_cell = (
476                      categories[i] != "broker"
477                      and categories[j] != "broker"
478                      and dsm.entities[i].split(".")[0] != dsm.entities[j].split(".")[0]
479                  )
480                  if check_cell and dsm.data[i][j] > 0:
481                      layered_architecture = False
482                      messages.append(
483                          f"Dependency from {dsm.entities[i]} to {dsm.entities[j]} breaks the layered architecture.",
484                      )
485  
486          return layered_architecture, "\n".join(messages)
487  
488  
489  class CodeClean(Checker):
490      """Code clean checker.
491  
492      Check that the number of issues per module is below a certain value.
493      """
494  
495      identifier = "archan.CodeClean"
496      name = "Code Clean"
497      description = """
498      The code base should be kept coherent and consistent. Complexity of
499      functions must not be too important. Conventions and standards must be used
500      in order to maintain a very human readable and maintainable code."""
501      hint = """
502      Reduce the number of issues in your code or increase the threshold."""
503  
504      argument_list = (Argument("threshold", int, "Message number threshold (per module).", default=10),)
505  
506      def check(
507          self,
508          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
509          **kwargs: Any,
510      ) -> tuple[Any, str]:
511          """Check code clean.
512  
513          Parameters:
514              dsm: The DSM to check.
515              **kwargs: Optional additional keyword arguments.
516  
517          Returns:
518              True if code clean else False, messages.
519          """
520          logger.debug(f"Entities = {dsm.entities}")
521          messages = []
522          code_clean = True
523          threshold = kwargs.pop("threshold", 1)
524          rows, _ = dsm.size
525          for i in range(rows):
526              if dsm.data[i][0] > threshold:
527                  messages.append(
528                      f"Number of issues ({dsm.data[i][0]}) in module {dsm.entities[i]} > threshold ({threshold})",
529                  )
530                  code_clean = False
531  
532          return code_clean, "\n".join(messages)