diff --git a/scripts/labels/invariant_check/rules/__init__.py b/scripts/labels/invariant_check/rules/__init__.py index c68d17d247..d2262cd133 100644 --- a/scripts/labels/invariant_check/rules/__init__.py +++ b/scripts/labels/invariant_check/rules/__init__.py @@ -9,15 +9,28 @@ This package contains the implementation for individually executable invariant verification rules. """ +from typing import List, Type + from .base import Base +from .guideline_implies_profile_security import \ + GuidelineImpliesProfileSecurity +from .guideline_requires_rule_number_annotation import \ + GuidelineRequiresRuleNumberAnnotation +from .guideline_rule_number_annotation_requires_guideline import \ + GuidelineRuleNumberAnnotationRequiresGuideline from .profile_default_subseteq_sensitive_subseteq_extreme \ import ProfileDefaultSubsetEqSensitiveSubsetEqExtreme +from .profile_no_alpha_checkers_in_production import \ + ProfileNoAlphaCheckersInProduction __all__ = [ "Base", + "GuidelineImpliesProfileSecurity", + "GuidelineRequiresRuleNumberAnnotation", + "GuidelineRuleNumberAnnotationRequiresGuideline", "ProfileDefaultSubsetEqSensitiveSubsetEqExtreme", + "ProfileNoAlphaCheckersInProduction", ] -rules_visible_to_user = [ - ProfileDefaultSubsetEqSensitiveSubsetEqExtreme, -] +rules_visible_to_user: List[Type[Base]] = \ + [globals()[cls_name] for cls_name in sorted(__all__) if cls_name != "Base"] diff --git a/scripts/labels/invariant_check/rules/base.py b/scripts/labels/invariant_check/rules/base.py index bf7e766c3f..98944e9a8d 100644 --- a/scripts/labels/invariant_check/rules/base.py +++ b/scripts/labels/invariant_check/rules/base.py @@ -20,7 +20,15 @@ class Base: supports_fixes = False @classmethod - def check(cls, labels: MultipleLabels, checker: str) \ + def supports_analyser(cls, analyser: str) -> bool: + """ + Returns whether the current guideline is applicable to checkers of + `analyser`. + """ + return True + + @classmethod + def check(cls, labels: MultipleLabels, analyser: str, checker: str) \ -> Tuple[bool, List[FixAction]]: """ Performs an implementation-specific routine that returns the diff --git a/scripts/labels/invariant_check/rules/guideline_implies_profile_security.py b/scripts/labels/invariant_check/rules/guideline_implies_profile_security.py new file mode 100644 index 0000000000..26c3078c2a --- /dev/null +++ b/scripts/labels/invariant_check/rules/guideline_implies_profile_security.py @@ -0,0 +1,38 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +from typing import List, Set, Tuple + +from ...checker_labels import MultipleLabels +from ... import fixit +from .base import Base + + +class GuidelineImpliesProfileSecurity(Base): + kind = "guideline.implies_profile_security" + description = """ +Ensures that checkers with a "guideline" label corresponding to a published +security guideline (e.g., SEI-CERT) are added to the 'security' profile. +""".replace('\n', ' ') + supports_fixes = True + + # Only the following guidelines will trigger the implication. + interesting_guidelines: Set[str] = {"sei-cert", + } + + @classmethod + def check(cls, labels: MultipleLabels, analyser: str, checker: str) \ + -> Tuple[bool, List[fixit.FixAction]]: + guidelines: Set[str] = set(labels[checker].get("guideline", list())) + if not guidelines & cls.interesting_guidelines: + return True, [] + + profiles: Set[str] = set(labels[checker].get("profile", list())) + missing_profiles = {"security"} - profiles + return not missing_profiles, \ + [fixit.AddLabelAction("profile:%s" % (profile)) + for profile in missing_profiles] diff --git a/scripts/labels/invariant_check/rules/guideline_requires_rule_number_annotation.py b/scripts/labels/invariant_check/rules/guideline_requires_rule_number_annotation.py new file mode 100644 index 0000000000..0494d7e32e --- /dev/null +++ b/scripts/labels/invariant_check/rules/guideline_requires_rule_number_annotation.py @@ -0,0 +1,46 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +from typing import List, Set, Tuple + +from ...checker_labels import MultipleLabels +from ...output import log, coloured, emoji +from ... import fixit +from .base import Base + + +class GuidelineRequiresRuleNumberAnnotation(Base): + kind = "guideline.requires_rule_number_annotation" + description = """ +Checks that checkers with a "guideline" label corresponding to a published +security guideline (e.g., SEI-CERT) must be labelled with a +":" label as well. +""".replace('\n', ' ') + supports_fixes = False + + # Only the following guidelines will trigger the check. + interesting_guidelines: Set[str] = {"sei-cert", + } + + @classmethod + def check(cls, labels: MultipleLabels, analyser: str, checker: str) \ + -> Tuple[bool, List[fixit.FixAction]]: + guidelines: Set[str] = set(labels[checker].get("guideline", list())) + + failed: List[str] = list() + for guideline in (guidelines & cls.interesting_guidelines): + if not labels[checker].get(guideline, list()): + log("%s%s: %s/%s - \"%s\" without \"%s\"", + emoji(":police_car_light: "), + coloured("RULE VIOLATION", "red"), + analyser, checker, + coloured("guideline:%s" % guideline, "green"), + coloured("%s:" % guideline, "red"), + ) + failed.append(guideline) + + return not failed, [] diff --git a/scripts/labels/invariant_check/rules/guideline_rule_number_annotation_requires_guideline.py b/scripts/labels/invariant_check/rules/guideline_rule_number_annotation_requires_guideline.py new file mode 100644 index 0000000000..7d3fa2184e --- /dev/null +++ b/scripts/labels/invariant_check/rules/guideline_rule_number_annotation_requires_guideline.py @@ -0,0 +1,56 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +from typing import List, Set, Tuple + +from ...checker_labels import MultipleLabels +from ...output import log, coloured, emoji +from ... import fixit +from .base import Base + + +class GuidelineRuleNumberAnnotationRequiresGuideline(Base): + kind = "guideline.rule_number_annotation_requires_guideline" + description = """ +Ensures that checkers with a ":" labels for a +published security guideline (e.g., SEI-CERT) must be labelled with a +"guideline:" label as well. +""".replace('\n', ' ') + supports_fixes = True + + # Only the following guidelines will trigger the check. + interesting_guidelines: Set[str] = {"sei-cert", + } + + @classmethod + def check(cls, labels: MultipleLabels, analyser: str, checker: str) \ + -> Tuple[bool, List[fixit.FixAction]]: + labelled_guidelines: Set[str] = set(labels[checker] + .get("guideline", list())) + missing_guidelines: List[str] = list() + + for guideline in cls.interesting_guidelines: + guideline_rule_annotations = set(labels[checker] + .get(guideline, list())) + if guideline_rule_annotations and \ + guideline not in labelled_guidelines: + missing_guidelines.append(guideline) + log("%s%s: %s/%s - \"%s\" without \"%s\"", + emoji(":police_car_light: "), + coloured("RULE VIOLATION", "red"), + analyser, checker, + "\", \"".join(( + coloured("%s:%s" % (guideline, rule_annotation), + "green") + for rule_annotation in guideline_rule_annotations + )), + coloured("guideline:%s" % (guideline), "red"), + ) + + return not missing_guidelines, \ + [fixit.AddLabelAction("guideline:%s" % (guideline)) + for guideline in missing_guidelines] diff --git a/scripts/labels/invariant_check/rules/profile_default_subseteq_sensitive_subseteq_extreme.py b/scripts/labels/invariant_check/rules/profile_default_subseteq_sensitive_subseteq_extreme.py index 97429c9203..dc7204d004 100644 --- a/scripts/labels/invariant_check/rules/profile_default_subseteq_sensitive_subseteq_extreme.py +++ b/scripts/labels/invariant_check/rules/profile_default_subseteq_sensitive_subseteq_extreme.py @@ -21,7 +21,7 @@ class ProfileDefaultSubsetEqSensitiveSubsetEqExtreme(Base): supports_fixes = True @classmethod - def check(cls, labels: MultipleLabels, checker: str) \ + def check(cls, labels: MultipleLabels, analyser: str, checker: str) \ -> Tuple[bool, List[fixit.FixAction]]: profiles: Set[str] = set(labels[checker].get("profile", list())) expected_profiles: Set[str] = set() diff --git a/scripts/labels/invariant_check/rules/profile_no_alpha_checkers_in_production.py b/scripts/labels/invariant_check/rules/profile_no_alpha_checkers_in_production.py new file mode 100644 index 0000000000..542a2b1320 --- /dev/null +++ b/scripts/labels/invariant_check/rules/profile_no_alpha_checkers_in_production.py @@ -0,0 +1,47 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +from typing import List, Set, Tuple + +from ...checker_labels import MultipleLabels +from ... import fixit +from .base import Base + + +class ProfileNoAlphaCheckersInProduction(Base): + kind = "profile.no_alpha_checkers_in_production_profiles" + description = """ +Ensures that Clang SA checkers in the 'alpha.' (and 'debug.') checker groups +do not belong to a production-grade "profile", e.g., 'default' or 'security'. +""".replace('\n', ' ') + supports_fixes = True + + # FIXME(v6.25?): It is planned that we will create a profile specifically + # for Alpha checkers that are not good enough to be possible to lift from + # Alpha status, but not bad enough to be completely unusable, in order to + # suggest ad-hoc use for interested clients. + # These groups **SHOULD** allow Alpha checkers. + profiles_allowing_alphas: Set[str] = {"", + } + + @classmethod + def supports_analyser(cls, analyser: str) -> bool: + return analyser == "clangsa" + + @classmethod + def check(cls, labels: MultipleLabels, analyser: str, checker: str) \ + -> Tuple[bool, List[fixit.FixAction]]: + if not cls.supports_analyser(analyser) \ + or not checker.startswith(("alpha.", "debug.")): + return True, [] + + profiles: Set[str] = set(labels[checker].get("profile", list())) + unexpected_profiles = profiles - cls.profiles_allowing_alphas + + return not unexpected_profiles, \ + [fixit.RemoveLabelAction("profile:%s" % (profile)) + for profile in unexpected_profiles] diff --git a/scripts/labels/invariant_check/tool/action.py b/scripts/labels/invariant_check/tool/action.py index 494882f4e3..4f411df12a 100644 --- a/scripts/labels/invariant_check/tool/action.py +++ b/scripts/labels/invariant_check/tool/action.py @@ -42,7 +42,8 @@ def verify_invariant(analyser: str, ok_rules, not_ok_rules = list(), list() for rule in rules: - rule_status, rule_fixes = rule.check({checker: labels}, checker) + rule_status, rule_fixes = rule.check({checker: labels}, + analyser, checker) if rule_status: if OutputSettings.report_ok(): log("%s%s/%s: %s %s", diff --git a/scripts/labels/invariant_check/tool/tool.py b/scripts/labels/invariant_check/tool/tool.py index 0b389619ae..23f5e3f858 100644 --- a/scripts/labels/invariant_check/tool/tool.py +++ b/scripts/labels/invariant_check/tool/tool.py @@ -27,7 +27,7 @@ class Statistics(NamedTuple): Analyser: str Checkers: int - Invariant: str + Rule: str OK: Optional[int] Not_OK: Optional[int] Fixed: Optional[int] @@ -60,11 +60,15 @@ def print_fixes(analyser: str, fixes: Dict[str, List[FixAction]]): plural(num_fixes, "fix is", "fixes are")) for checker in sorted(fixes.keys()): + checker_fixes = fixes[checker] + if not checker_fixes: + continue + log(" %s· %s", emoji(":magnifying_glass_tilted_right: "), coloured(checker, "cyan")) - for fix in fixes[checker]: + for fix in checker_fixes: if isinstance(fix, AddLabelAction): fix_str = "+ %s" % (coloured(fix.new, "green")) elif isinstance(fix, ModifyLabelAction): @@ -72,7 +76,7 @@ def print_fixes(analyser: str, fixes: Dict[str, List[FixAction]]): coloured(fix.old, "red"), coloured(fix.new, "green")) elif isinstance(fix, RemoveLabelAction): - fix_str = "- %s" % (coloured(fix.new, "red")) + fix_str = "- %s" % (coloured(fix.old, "red")) else: fix_str = coloured(str(fix), "magenta") @@ -98,6 +102,7 @@ def execute(analyser: str, stats: List[Statistics] = list() fixes: FixMap = dict() + rules = [rule for rule in rules if rule.supports_analyser(analyser)] with Pool(max_workers=process_count) as pool: ok_rules, not_ok_rules, fixing_rules, fixes = \ action.run_check(pool, analyser, rules, labels) @@ -105,7 +110,7 @@ def execute(analyser: str, for rule in rules: stats.append(Statistics(Analyser=analyser, Checkers=len(labels), - Invariant=rule.kind, + Rule=rule.kind, OK=ok_rules.get(rule.kind, None), Not_OK=not_ok_rules.get(rule.kind, None), Fixed=fixing_rules.get(rule.kind, None),