diff --git a/MODULE.bazel b/MODULE.bazel index 9eb81cb64..34db31e1a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -94,5 +94,12 @@ http_file( bazel_dep(name = "score_process_description", version = "2.1.2") +# Security process checks (#796) are not yet released +git_override( + module_name = "score_process_description", + commit = "5f80e45c89e99e1c964e556c93ff8acfb19c6718", + remote = "https://github.com/eclipse-score/process_description.git", +) + # Provide the tools from the devcontainer to Bazel bazel_dep(name = "score_devcontainer", version = "1.11.1") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 49bbb0483..4d4655256 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -286,8 +286,6 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_shell/0.4.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_devcontainer/1.11.1/MODULE.bazel": "a4d27e121bdee44906e0eac49a74427446482ce16158827802b29c7c3d378150", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_devcontainer/1.11.1/source.json": "0be271286a80b5554628884b1835d6a7b95ce8b9586b572600da95b6c57be428", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process_description/2.1.2/MODULE.bazel": "1076b36d2d05ab1a18df0746f6a545869eec6927019d651af99d99ef056e2023", - "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process_description/2.1.2/source.json": "f76099f076243e2641803971e2a95ed27f24756c9bdda5d10a808c664996f0fc", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/stardoc/0.5.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/stardoc/0.5.3/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/stardoc/0.5.6/MODULE.bazel": "not found", diff --git a/docs/internals/requirements/requirements.rst b/docs/internals/requirements/requirements.rst index a2947951e..d26789d1e 100644 --- a/docs/internals/requirements/requirements.rst +++ b/docs/internals/requirements/requirements.rst @@ -647,7 +647,7 @@ Architecture Attributes .. tool_req:: Security: Restrict linkage :id: tool_req__docs_arch_link_security :tags: Architecture - :implemented: YES + :implemented: PARTIAL :version: 1 :parent_covered: YES :satisfies: gd_req__arch_linkage_security_trace[version==1] @@ -655,6 +655,49 @@ Architecture Attributes Docs-as-Code shall enforce that security relevant :need:`tool_req__docs_arch_types` (Security == YES) can only be linked against security relevant :need:`tool_req__docs_arch_types`. + .. note:: + Currently only the ``implements`` link is checked. + +.. tool_req:: Security: Requirement satisfied by security architecture + :id: tool_req__docs_req_link_security_to_arch + :tags: Architecture + :implemented: YES + :version: 1 + :parent_covered: YES + :satisfies: gd_req__arch_linkage_requirement_security[version==1] + + Docs-as-Code shall enforce that security relevant requirements (Security == YES) are only + satisfied (``satisfied_by``) by security relevant :need:`tool_req__docs_arch_types` + (Security == YES). + +.. tool_req:: Security: Non-security architecture fulfils no security requirement + :id: tool_req__docs_arch_link_nonsec_to_sec_req + :tags: Architecture + :implemented: YES + :version: 1 + :parent_covered: YES + :satisfies: gd_req__arch_linkage_requirement_security[version==1] + + Docs-as-Code shall enforce that :need:`tool_req__docs_arch_types` which are not security + relevant (Security == NO) do not fulfil (``fulfils``) security relevant requirements or AoUs + (Security == YES). + +.. tool_req:: Security: Security requirement keeps a security child + :id: tool_req__docs_req_link_security_child + :tags: Architecture + :implemented: YES + :version: 1 + :parent_covered: YES + :satisfies: gd_req__req_linkage_security[version==1] + + Docs-as-Code shall enforce that every security relevant requirement (Security == YES) which + has child requirements (``derived_from``) has at least one security relevant child requirement + (Security == YES). + + .. note:: + Parent-child pairs across repository boundaries are not checked, because either the + children are missing in the build or the parent is external. + ---------------------- 🖼️ Diagram Related ---------------------- diff --git a/src/extensions/score_metamodel/checks/graph_checks.py b/src/extensions/score_metamodel/checks/graph_checks.py index b6c854956..8cbe75b25 100644 --- a/src/extensions/score_metamodel/checks/graph_checks.py +++ b/src/extensions/score_metamodel/checks/graph_checks.py @@ -11,6 +11,7 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* import operator +from collections import defaultdict from collections.abc import Callable from functools import reduce from itertools import chain @@ -183,6 +184,8 @@ def check_metamodel_graph( f"Explanation for graph check {check_name} is missing. " "Explanations are mandatory for graph checks." ) + # New checks only report infos until existing data has been cleaned up. + is_new_check = bool(check_config.get("new_check", False)) # Get all needs matching the selection criteria try: selected_needs = filter_needs_by_criteria( @@ -224,7 +227,7 @@ def check_metamodel_graph( f"condition `{check_to_perform[parent_relation]}`." f" Explanation: {explanation}" ) - log.warning_for_need(need, msg) + log.warning_for_need(need, msg, is_new_check=is_new_check) @graph_check @@ -254,3 +257,37 @@ def check_valid_only_links_to_valid( if invalid_needs: msg = f"is valid but links to invalid need(s): {invalid_needs}" log.warning_for_need(need, msg, is_new_check=True) + + +# req-Id: tool_req__docs_req_link_security_child +@graph_check +def check_security_parent_keeps_security_child( + app: Sphinx, + all_needs: NeedsView, + log: CheckLogger, +): + """ + A security relevant requirement with child requirements (derived_from) + must keep at least one security relevant child. Unlike safety, children + which are not security relevant are allowed, the security aspect must just + not get lost during refinement. + """ + req_types = {"stkh_req", "feat_req", "comp_req"} + children: defaultdict[str, list[NeedItem]] = defaultdict(list) + for need in all_needs.values(): + if need["type"] in req_types: + parents = cast(list[str], need.get("derived_from") or []) + for parent in parents: + # Strip version conditions like `[version==1]` + children[parent.split("[")[0]].append(need) + + for need in all_needs.filter_is_external(False).values(): + if need["type"] not in req_types or need.get("security") != "YES": + continue + kids = children.get(need["id"], []) + if kids and not any(k.get("security") == "YES" for k in kids): + msg = ( + "is security relevant, but none of its child requirements is: " + + ", ".join(k["id"] for k in kids) + ) + log.warning_for_need(need, msg, is_new_check=True) diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index fa318b3d1..1a8f22014 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -1253,10 +1253,10 @@ graph_checks: # req-Id: tool_req__docs_arch_link_security tool_req__docs_arch_link_security: needs: - include: feat_arc_sta, logic_arc_int, logic_arc_int_op, comp_arc_sta, real_arc_int, real_arc_int_op + include: feat_arc_sta, logic_arc_int, logic_arc_int_op, comp, comp_arc_sta, real_arc_int, real_arc_int_op condition: security == YES check: - implements: security == YES # Which attribute??? + implements: security == YES explanation: An security architecture element can only link other security architecture elements. # Workproducts may only link to ASPICE 40 IIC stakeholder requirements @@ -1270,3 +1270,25 @@ graph_checks: - id contains aspice_40__iic - id contains std_wp explanation: Workproducts may only link to ASPICE 40 IIC stakeholder requirements. Please ensure that the linked requirement is an ASPICE 40 IIC stakeholder requirement. + + # req-Id: tool_req__docs_req_link_security_to_arch + tool_req__docs_req_link_security_to_arch: + needs: + include: feat_req, comp_req + condition: security == YES + check: + satisfied_by: security == YES + # Reported as info until existing data has been cleaned up. + new_check: true + explanation: A security relevant requirement can only be satisfied by security relevant architecture elements. + + # req-Id: tool_req__docs_arch_link_nonsec_to_sec_req + tool_req__docs_arch_link_nonsec_to_sec_req: + needs: + include: feat_arc_sta, feat_arc_dyn, logic_arc_int, comp, comp_arc_sta, comp_arc_dyn, real_arc_int + condition: security == NO + check: + fulfils: security == NO + # Reported as info until existing data has been cleaned up. + new_check: true + explanation: An architecture element which is not security relevant cannot fulfil security relevant requirements or AoUs. diff --git a/src/extensions/score_metamodel/tests/rst/graph/test_invalid_graph.rst b/src/extensions/score_metamodel/tests/rst/graph/test_invalid_graph.rst index 348d3dda3..aa08d8a50 100644 --- a/src/extensions/score_metamodel/tests/rst/graph/test_invalid_graph.rst +++ b/src/extensions/score_metamodel/tests/rst/graph/test_invalid_graph.rst @@ -20,7 +20,7 @@ :derivation_technique: requirements_based Checks if valid reqs only link to valid reqs - Note: DISABLED ATM, due to check not being a 'full' warning yet + Note: The check is a new check, so its finding is reported as info. @@ -31,13 +31,9 @@ -.. We can not yet enable this test. As the check is only an 'info' and not yet a true warning -.. Therefore the test is the inverse of what we will test once it is enabled. - - .. comp_saf_fmea:: Child requirement :id: comp_saf_fmea__child__1 :safety: QM :status: valid :mitigated_by: feat_req__parent__QM_invalid - :expect_not: invalid need(s) + :expect: is valid but links to invalid need(s): {'feat_req__parent__QM_invalid'} diff --git a/src/extensions/score_metamodel/tests/rst/graph/test_security_linkage.rst b/src/extensions/score_metamodel/tests/rst/graph/test_security_linkage.rst new file mode 100644 index 000000000..96e209578 --- /dev/null +++ b/src/extensions/score_metamodel/tests/rst/graph/test_security_linkage.rst @@ -0,0 +1,173 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +.. test_metadata:: + :id: test_metadata__security_linkage + :partially_verifies_list: tool_req__docs_arch_link_security, tool_req__docs_req_link_security_to_arch, tool_req__docs_arch_link_nonsec_to_sec_req, tool_req__docs_req_link_security_child + :test_type: requirements_based + :derivation_technique: requirements_based + + Tests the security linkage checks between requirements and architecture. + The checks tool_req__docs_req_link_security_to_arch, + tool_req__docs_arch_link_nonsec_to_sec_req and + tool_req__docs_req_link_security_child are new checks, so their findings + are reported as infos instead of warnings. + + +.. Setup: link targets used by the tests below. + +.. logic_arc_int:: Security interface + :id: logic_arc_int__test_sec__yes + :security: YES + :safety: QM + :status: valid + +.. logic_arc_int:: Non-security interface + :id: logic_arc_int__test_sec__no + :security: NO + :safety: QM + :status: valid + +.. comp:: Security component + :id: comp__test_sec_yes + :security: YES + :safety: QM + :status: valid + +.. comp:: Non-security component + :id: comp__test_sec_no + :security: NO + :safety: QM + :status: valid + + +.. tool_req__docs_arch_link_security: a security component may only implement security interfaces. + +.. Positive Test: security component implements a security interface. + +.. comp:: Security component implements security interface + :id: comp__test_sec_impl_ok + :security: YES + :safety: QM + :status: valid + :implements: logic_arc_int__test_sec__yes + :expect_not: does not fulfill condition `security == YES` + +.. Negative Test: security component implements a non-security interface. + +.. comp:: Security component implements non-security interface + :id: comp__test_sec_impl_bad + :security: YES + :safety: QM + :status: valid + :implements: logic_arc_int__test_sec__no + :expect: Parent need `logic_arc_int__test_sec__no` does not fulfill condition `security == YES`. + + +.. tool_req__docs_req_link_security_to_arch: new check, reported as info only. + +.. Positive Test: security requirement satisfied by a security component. + +.. comp_req:: Security requirement + :id: comp_req__test_sec_yes + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__test_sec_yes + :expect_not: does not fulfill condition `security == YES` + +.. Negative Test: security requirement satisfied by a non-security component. + +.. comp_req:: Security requirement satisfied by non-security component + :id: comp_req__test_sec_satisfied_by_nonsec + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__test_sec_no + :expect: Parent need `comp__test_sec_no` does not fulfill condition `security == YES` + + +.. tool_req__docs_arch_link_nonsec_to_sec_req: new check, reported as info only. + +.. comp_req:: Non-security requirement + :id: comp_req__test_sec_no + :security: NO + :safety: QM + :status: valid + :satisfied_by: comp__test_sec_no + +.. Positive Test: non-security interface fulfils a non-security requirement. + +.. real_arc_int:: Non-security interface fulfils non-security requirement + :id: real_arc_int__test_sec__fulfils_ok + :security: NO + :safety: ASIL_B + :status: valid + :fulfils: comp_req__test_sec_no + :expect_not: does not fulfill condition `security == NO` + +.. Negative Test: non-security interface fulfils a security requirement. + +.. real_arc_int:: Non-security interface fulfils security requirement + :id: real_arc_int__test_sec__fulfils_bad + :security: NO + :safety: ASIL_B + :status: valid + :fulfils: comp_req__test_sec_yes + :expect: Parent need `comp_req__test_sec_yes` does not fulfill condition `security == NO` + + +.. tool_req__docs_req_link_security_child: new check, reported as info only. + +.. Positive Test: one security child is enough, non-security children are allowed. + +.. feat_req:: Security parent with a security child + :id: feat_req__test_sec_parent_ok + :security: YES + :safety: QM + :status: valid + :expect_not: none of its child requirements + +.. comp_req:: Security child + :id: comp_req__test_sec_child_ok_yes + :security: YES + :safety: QM + :status: valid + :derived_from: feat_req__test_sec_parent_ok + :satisfied_by: comp__test_sec_yes + +.. comp_req:: Non-security child next to a security child + :id: comp_req__test_sec_child_ok_no + :security: NO + :safety: QM + :status: valid + :derived_from: feat_req__test_sec_parent_ok + :satisfied_by: comp__test_sec_no + +.. Negative Test: security parent with only non-security children. + +.. feat_req:: Security parent with only non-security children + :id: feat_req__test_sec_parent + :security: YES + :safety: QM + :status: valid + :expect: is security relevant, but none of its child requirements is: comp_req__test_sec_child_no + +.. comp_req:: Non-security child + :id: comp_req__test_sec_child_no + :security: NO + :safety: QM + :status: valid + :derived_from: feat_req__test_sec_parent + :satisfied_by: comp__test_sec_no diff --git a/src/extensions/score_metamodel/tests/test_rules_file_based.py b/src/extensions/score_metamodel/tests/test_rules_file_based.py index 00998d472..27833b38a 100644 --- a/src/extensions/score_metamodel/tests/test_rules_file_based.py +++ b/src/extensions/score_metamodel/tests/test_rules_file_based.py @@ -144,12 +144,16 @@ def filter_warnings_by_position( ) -> list[str]: """ Filtering only warnings that belong to this file & line. But also deleting the prefix. - Filter out the filepath:linenr prefix from warning. So that the 'expect-not' can be generic + Filter out the filepath:linenr prefix (and WARNING: for warnings, infos of new checks have none). So that the 'expect-not' can be generic Without having to pay attention to the filename for example 'EXPECT-NOT: test' then matching a random warning because 'test' is in the filename of 'graph/test_graph_checks.rst' """ - prefix = f"{rst_data.filename}:{line_nr}: WARNING:" - return [warning.removeprefix(prefix) for warning in warnings if prefix in warning] + prefix = f"{rst_data.filename}:{line_nr}:" + return [ + warning.split(prefix, 1)[1].removeprefix(" WARNING:") + for warning in warnings + if prefix in warning + ] def warning_matches( @@ -246,6 +250,8 @@ def _collect_warnings(app: SphinxTestApp) -> list[str]: ### Return cleaned build warnings, failing fast on unknown-option errors. # Some warnings are suppressed in conf.py, so the set here is already limited. warnings = [strip_ansi_codes(w) for w in app.warning.getvalue().splitlines()] + # New checks report infos instead of warnings, they are expected in the same way. + warnings += [strip_ansi_codes(w) for w in app.status.getvalue().splitlines()] unknown_option = [w for w in warnings if "unknown option" in w.lower()] if unknown_option: pytest.fail(