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)