diff --git a/artefact_enumerator.py b/artefact_enumerator.py index bb492fc9..251568ff 100644 --- a/artefact_enumerator.py +++ b/artefact_enumerator.py @@ -359,6 +359,23 @@ def _process_compliance_snapshot_of_artefact( if uncommitted_backlog_item: uncommitted_backlog_items.append(uncommitted_backlog_item) + if ( + extensions_cfg.test_results + and extensions_cfg.test_results.enabled + and extensions_cfg.test_results.is_supported(artefact_kind=artefact.artefact_kind) + ): + compliance_snapshot, uncommitted_backlog_item = _create_backlog_item_for_extension( + finding_cfgs=finding_cfgs, + finding_types=(odg.model.Datatype.TEST_RESULT_FINDING,), + artefact=artefact, + compliance_snapshot=compliance_snapshot, + service=odg.extensions_cfg.Services.TEST_RESULT_FINDING, + interval_seconds=extensions_cfg.osid.interval, + now=now, + ) + if uncommitted_backlog_item: + uncommitted_backlog_items.append(uncommitted_backlog_item) + if ( extensions_cfg.osid and extensions_cfg.osid.enabled diff --git a/charts/extensions/Chart.yaml b/charts/extensions/Chart.yaml index c00b98a7..22b29999 100644 --- a/charts/extensions/Chart.yaml +++ b/charts/extensions/Chart.yaml @@ -44,3 +44,6 @@ dependencies: - name: odg-operator repository: file://charts/odg-operator condition: odg-operator.enabled + - name: test-evidence + repository: file://charts/test-evidence + condition: test-evidence.enabled diff --git a/charts/extensions/charts/test-evidence/templates/test-results.yaml b/charts/extensions/charts/test-evidence/templates/test-results.yaml new file mode 100644 index 00000000..37729176 --- /dev/null +++ b/charts/extensions/charts/test-evidence/templates/test-results.yaml @@ -0,0 +1,123 @@ +{{- $podName := "test-evidence" }} + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $podName }} + namespace: {{ .Values.target_namespace | default .Release.Namespace }} + {{- if default dict (.Values.deployment).annotations }} + annotations: + {{- range $annotation, $value := .Values.deployment.annotations }} + {{ $annotation }}: {{ $value }} + {{- end }} + {{- end }} +spec: + replicas: 0 # will be scaled automatically by backlog-controller + selector: + matchLabels: + app: {{ $podName }} + delivery-gear.gardener.cloud/service: testEvidence + template: + metadata: + labels: + app: {{ $podName }} + delivery-gear.gardener.cloud/service: testEvidence + spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ $podName }} + terminationGracePeriodSeconds: 300 # 5 min + containers: + - name: {{ $podName }} + image: {{ include "image" .Values.image }} + imagePullPolicy: IfNotPresent + command: + - python3 + - -m + - test_evidence_extension + securityContext: + allowPrivilegeEscalation: false + env: + - name: SECRET_FACTORY_PATH + value: /secrets + - name: EXTENSIONS_CFG_PATH + value: /extensions_cfg/extensions_cfg + - name: FINDINGS_CFG_PATH + value: /findings_cfg/findings_cfg + - name: OCM_REPO_MAPPINGS_PATH + value: /ocm_repo_mappings/ocm_repo_mappings + - name: K8S_TARGET_NAMESPACE + value: {{ .Values.target_namespace | default .Release.Namespace }} + volumeMounts: + - name: github + mountPath: /secrets/github + - name: github-app + mountPath: /secrets/github-app + - name: kubernetes + mountPath: /secrets/kubernetes + - name: oci-registry + mountPath: /secrets/oci-registry + - name: extensions-cfg + mountPath: /extensions_cfg + - name: findings-cfg + mountPath: /findings_cfg + - name: ocm-repo-mappings + mountPath: /ocm_repo_mappings + readOnly: true + lifecycle: + preStop: # hook ensures that just created pods have at least enough time alive to add a termination signal handler + exec: + command: + - sleep + - "60" + resources: + requests: + memory: 100Mi + cpu: 250m + limits: + memory: 300Mi + cpu: 500m + volumes: + - name: github + secret: + secretName: secret-factory-github + optional: true + - name: github-app + secret: + secretName: secret-factory-github-app + optional: true + - name: kubernetes + secret: + secretName: secret-factory-kubernetes + optional: true # might use incluster config + - name: oci-registry + secret: + secretName: secret-factory-oci-registry + optional: true # oci authentication is optional + - name: extensions-cfg + configMap: + name: extensions-cfg + - name: findings-cfg + configMap: + name: findings-cfg + - name: ocm-repo-mappings + configMap: + name: ocm-repo-mappings +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-egress-from-test-evidence + namespace: {{ .Values.target_namespace | default .Release.Namespace }} +spec: + podSelector: + matchLabels: + app: {{ $podName }} + policyTypes: + - Egress + egress: + - {} # Allows all egress traffic to any destination and port diff --git a/charts/extensions/values.yaml b/charts/extensions/values.yaml index 2617b786..8773186b 100644 --- a/charts/extensions/values.yaml +++ b/charts/extensions/values.yaml @@ -88,3 +88,10 @@ sast: image: repository: null tag: null +test-evidence: + deployment: + annotations: [] + enabled: false + image: + repository: null + tag: null diff --git a/docs/extensions/evidence-checker.jpg b/docs/extensions/evidence-checker.jpg new file mode 100644 index 00000000..c106429d Binary files /dev/null and b/docs/extensions/evidence-checker.jpg differ diff --git a/docs/extensions/evidence_checker.rst b/docs/extensions/evidence_checker.rst new file mode 100644 index 00000000..c8865cfd --- /dev/null +++ b/docs/extensions/evidence_checker.rst @@ -0,0 +1,64 @@ +Evidence Checker Extension +========================== + +Purpose +------- + +Tests are a crucial part of software development, making sure the software's functionality stays intact despite the introduction of new features and enhancements. To prove the software's stability it is therefore often required to document and store test results of unit tests. + +Therefore, the aim of the evidence checker extension is to create a finding for an artefact/resource in case no test evidences from unit tests have been found for this artefact. + +Labels used for the extension +----------------------------- + +The extension uses three labels in your component descriptor as described below. + +1. Purpose Label +~~~~~~~~~~~~~~~~ + +The purpose label defines the purpose of the artefact respectively what the artefact is used for. The value of the purpose label could be such as 'lint', 'sast' or 'test'. E.g., if the value is 'test' we know that this artefact is in fact a test evidence. + +.. code-block:: yaml + + - name: gardener.cloud/purposes + signing: false + value: + - test + +2. Test Policy Label +~~~~~~~~~~~~~~~~~~~~ + +The test policy label defines whether an artefact requires unit tests - and therefore test evidences - or not. E.g., a helmchart does not require unit tests, while an OCI-image would require unit tests. + +.. code-block:: yaml + + - name: gardener.cloud/test-policy + value: false / true + +3. Test Scope Label +~~~~~~~~~~~~~~~~~~~ + +The test scope label is set in the component descriptor of a test artefact (aka an artefact whose purpose label has the value 'test) and it defines for which other artefacts it represents the test evidence. + +If you do not specify any value in this label it is automatically assumed that the test artefact is valid for all artefacts within a component. + +.. code-block:: yaml + + - name: gardener.cloud/test-scope + value: + - artefact-1 + - artefact-2 + +Functionality +------------- + +1. The extension scans each artefact of a component and first validates whether the currently scanned artefact is a test evidence itself or not. This is achieved with the use of the gardener.cloud/purposes label. An artefact is considered a test evidence provided the label's value is 'test'. + +2. Provided the gardener.cloud/purposes label's has NOT been given the value 'test' the extension further checks, whether the artefact requires a test or not. This is achieved with the gardener.cloud/test-policy label which is either 'true' or 'false. + +3. In case an artefact has been identified as a test artefact (as described in step 1) the extension further checks, for which artefacts within the component the test-evidence is valid for. This can be defined with the help of the gardener.cloud/test-scope label. + +4. In case one of the artefacts identified in step 2 is not covered by the test artefacts(aka evidences) a finding will be created and test-evidences will have to be provided retrospectively. + +.. image:: evidence-checker.jpg + :alt: Evidence Checker Flow diff --git a/odg/extensions_cfg.py b/odg/extensions_cfg.py index 1846d0a0..8e8f89c6 100644 --- a/odg/extensions_cfg.py +++ b/odg/extensions_cfg.py @@ -43,6 +43,7 @@ class Services(enum.StrEnum): PPMS = 'ppms' RESPONSIBLES = 'responsibles' SAST = 'sast' + TEST_EVIDENCE = 'testEvidence' ODG_OPERATOR = 'odg-operator' SBOM_GENERATOR = 'sbomGenerator' @@ -984,6 +985,24 @@ def is_supported( return True +@dataclasses.dataclass(kw_only=True) +class TestEvidenceConfig(BacklogItemMixins): + service: Services = Services.TEST_EVIDENCE + delivery_service_url: str + external_artefacts_require_tests: bool + interval: int = 60 * 60 * 24 # + on_unsupported: WarningVerbosities = WarningVerbosities.WARNING + + def is_supported( + self, + artefact_kind: odg.model.ArtefactKind, + ) -> bool: + if artefact_kind is not odg.model.ArtefactKind.RESOURCE: + return False + + return True + + @dataclasses.dataclass(kw_only=True) class OsId(BacklogItemMixins): ''' @@ -1119,6 +1138,7 @@ class ExtensionsConfiguration: responsibles: ResponsiblesConfig | None sast: SASTConfig | None sbom_generator: SBOMGeneratorConfig | None + test_evidence: TestEvidenceConfig | None backlog_controller: BacklogControllerConfig = dataclasses.field(default_factory=BacklogControllerConfig) # noqa: E501 @staticmethod diff --git a/odg/extensions_cfg.yaml b/odg/extensions_cfg.yaml index 155dd62b..c8f88827 100644 --- a/odg/extensions_cfg.yaml +++ b/odg/extensions_cfg.yaml @@ -105,3 +105,6 @@ sbom_generator: mappings: - prefix: '' group_id: 0 # must be set + +test_evidence: + external_artefacts_require_tests: False diff --git a/odg/findings.py b/odg/findings.py index b6ce79d6..bd53212b 100644 --- a/odg/findings.py +++ b/odg/findings.py @@ -135,6 +135,15 @@ class SASTFindingSelector: sub_types: list[str] +@dataclasses.dataclass +class TestEvidenceFindingSelector: + ''' + :param list[str] status: + List of regexes to determine matching test-evidence statuses. + ''' + status: list[str] + + @dataclasses.dataclass class VulnerabilityFindingSelector: ''' @@ -195,6 +204,7 @@ class FindingCategorisation: | SASTFindingSelector | VulnerabilityFindingSelector | OsIdFindingSelector + | TestEvidenceFindingSelector | None ) @@ -557,6 +567,8 @@ def _validate(self): self._validate_malware() case odg.model.Datatype.SAST_FINDING: self._validate_sast() + case odg.model.Datatype.TEST_EVIDENCE_FINDING: + self._validate_test_result() case odg.model.Datatype.VULNERABILITY_FINDING: self._validate_vulnerabilty() case odg.model.Datatype.INVENTORY_FINDING: @@ -647,6 +659,18 @@ def _validate_malware(self): e.add_note('\n'.join(violations)) raise e + def _validate_test_result(self): + violations = self._validate_categorisations( + expected_selector=TestEvidenceFindingSelector, + ) + + if not violations: + return + + e = ModelValidationError('test evidence violations found:') + e.add_note('\n'.join(violations)) + raise e + def _validate_sast(self): violations = self._validate_categorisations( expected_selector=SASTFindingSelector, @@ -930,3 +954,8 @@ def categorise_finding( for status in selector.status: if re.fullmatch(status, finding_property, re.IGNORECASE): return categorisation + + elif isinstance(selector, TestEvidenceFindingSelector): + for status in selector.status: + if re.fullmatch(status, finding_property, re.IGNORECASE): + return categorisation diff --git a/odg/findings_cfg.yaml b/odg/findings_cfg.yaml index 726d9fa3..9d8dcbe4 100644 --- a/odg/findings_cfg.yaml +++ b/odg/findings_cfg.yaml @@ -46,6 +46,25 @@ malware_names: - .* +- type: finding/test-evidence + issues: + enable_assignees: False + categorisations: + - id: NONE + display_name: No Tests required + value: 0 + allowed_processing_time: ~ + rescoring: manual + + - id: BLOCKER + display_name: No Test Evidence + value: 16 + allowed_processing_time: 0 + rescoring: manual + selector: + status: + - .* + - type: finding/sast issues: enable_issues: False diff --git a/odg/labels.py b/odg/labels.py index a011a9a4..8bc17419 100644 --- a/odg/labels.py +++ b/odg/labels.py @@ -76,6 +76,18 @@ class CveCategorisationLabel(Label): value: odg.cvss.CveCategorisation +@dataclasses.dataclass(frozen=True) +class TestScopeLabel(Label): + name = 'gardener.cloud/test-scope' + value: str # artefact-name + + +@dataclasses.dataclass(frozen=True) +class TestPolicyLabel(Label): + name = 'gardener.cloud/test-policy' + value: bool # is test required + + @functools.cache def _label_to_type() -> dict[str, Label]: own_module = sys.modules[__name__] diff --git a/odg/model.py b/odg/model.py index 1f20f593..bb7351ec 100644 --- a/odg/model.py +++ b/odg/model.py @@ -42,6 +42,7 @@ class Datatype(enum.StrEnum): OSID_FINDING = 'finding/osid' SAST_FINDING = 'finding/sast' VULNERABILITY_FINDING = 'finding/vulnerability' + TEST_EVIDENCE_FINDING = 'finding/test-evidence' # informational datatypes CRYPTO_ASSET = 'crypto_asset' @@ -62,6 +63,7 @@ def datasource(self) -> 'Datasource': Datatype.OSID_FINDING: Datasource.OSID, Datatype.SAST_FINDING: Datasource.SAST, Datatype.VULNERABILITY_FINDING: Datasource.BDBA, + Datatype.TEST_EVIDENCE_FINDING: Datasource.TEST_EVIDENCE, }[self] def display_name(self) -> str: @@ -83,6 +85,7 @@ class Datasource(enum.StrEnum): OSID = 'osid' RESPONSIBLES = 'responsibles' SAST = 'sast' + TEST_EVIDENCE = 'test-evidence' def datatypes(self) -> tuple[Datatype, ...]: return { @@ -126,6 +129,7 @@ def datatypes(self) -> tuple[Datatype, ...]: Datasource.SAST: ( Datatype.SAST_FINDING, ), + Datasource.TEST_EVIDENCE: (Datatype.TEST_EVIDENCE_FINDING) }.get(self, tuple()) @@ -177,13 +181,23 @@ class UserIdentity: ''' Collection of identities that refer to the same user ''' - identifiers: list[EmailAddress | GithubUser | MetaOrigin | PersonalName | UserIdentifierBase] + identifiers: list[ + EmailAddress + | GithubUser + | MetaOrigin + | PersonalName + | UserIdentifierBase + ] class SastStatus(enum.StrEnum): NO_LINTER = 'no-linter' +class TestStatus(enum.StrEnum): + NO_TEST_EVIDENCE = 'no-test-evidence' + + class SastSubType(enum.StrEnum): LOCAL_LINTING = 'local-linting' CENTRAL_LINTING = 'central-linting' @@ -473,6 +487,15 @@ def key(self) -> str: return _as_key(self.package_name, self.package_version, self.license.name) +@dataclasses.dataclass +class TestEvidenceMissingFinding(Finding): + test_status: TestStatus + + @property + def key(self) -> str: + return _as_key(self.test_status) + + @dataclasses.dataclass class VulnerabilityFinding(Finding, BDBAMixin): cve: str @@ -1426,6 +1449,7 @@ def key(self) -> str: | OsIdFinding | SastFinding | VulnerabilityFinding + | TestEvidenceMissingFinding ) InformationalModels = ( StructureInfo diff --git a/odg/profiles.yaml b/odg/profiles.yaml index 8d241e13..89fc4cd5 100644 --- a/odg/profiles.yaml +++ b/odg/profiles.yaml @@ -12,5 +12,6 @@ - finding/osid - finding/sast - finding/vulnerability + - finding/test-result special_component_ids: - 03e237b4-434e-4e1c-b786-5ceb6cc76c1c diff --git a/test/resources/test_evidence_component_descriptor.yaml b/test/resources/test_evidence_component_descriptor.yaml new file mode 100644 index 00000000..7c643652 --- /dev/null +++ b/test/resources/test_evidence_component_descriptor.yaml @@ -0,0 +1,45 @@ +component: + name: MyComponent + version: 1.2.3 + resources: + - name: test-evidence + relation: local + labels: + - name: gardener.cloud/purposes + value: + - test + - name: gardener.cloud/test-scope + value: build-result + version: foo + type: foo + + - name: build-result + relation: local + labels: + - name: gardener.cloud/test-policy + value: True + version: foo + type: foo + + - name: other-build-result + relation: local + labels: + - name: gardener.cloud/test-policy + value: True + version: foo + type: foo + + - name: build-result-not-requiring-tests + relation: local + labels: + - name: gardener.cloud/test-policy + value: False + version: foo + type: foo + + repositoryContexts: [] + provider: any + sources: [] + componentReferences: [] +meta: + schemaVersion: v2 \ No newline at end of file diff --git a/test/test_test_evidence_extension.py b/test/test_test_evidence_extension.py new file mode 100644 index 00000000..48e65250 --- /dev/null +++ b/test/test_test_evidence_extension.py @@ -0,0 +1,112 @@ +import enum +import os +import pathlib + +import dacite +import pytest +import yaml + +import ocm +import ocm.iter + +import odg.extensions_cfg +import test_evidence_extension as tee + + +parent_dir = pathlib.Path(__file__).parent +resource_dir = os.path.abspath( + os.path.join( + parent_dir, + 'resources', + ), +) + + +@pytest.fixture +def component() -> ocm.Component: + path = os.path.join( + resource_dir, + 'test_evidence_component_descriptor.yaml', + ) + with open(path, 'r') as f: + raw = yaml.safe_load(f) + + return dacite.from_dict( + data=raw, + data_class=ocm.ComponentDescriptor, + config=dacite.Config(cast=[enum.Enum]) + ).component + + +@pytest.fixture +def extensions_cfg() -> odg.extensions_cfg.TestEvidenceConfig: + return odg.extensions_cfg.TestEvidenceConfig( + delivery_service_url='foo', + external_artefacts_require_tests=False, + ) + + +def _find_resource_node( + component: ocm.Component, + resource_name: str, +) -> ocm.iter.ResourceNode | None: + for rnode in ocm.iter.iter( + component=component, + recursion_depth=0, + node_filter=ocm.iter.Filter.resources, + ): + rnode: ocm.iter.ResourceNode + if rnode.resource.name == resource_name: + return rnode + + raise ValueError(f'did not find resource for {resource_name=}') + + +def test_is_test_required( + component: ocm.Component, + extensions_cfg: odg.extensions_cfg.TestEvidenceConfig, +): + artefact_not_requiring_tests = _find_resource_node( + component=component, + resource_name='build-result-not-requiring-tests', + ) + assert tee.is_test_required( + artefact_node=artefact_not_requiring_tests, + extensions_cfg=extensions_cfg, + ) is False + + artefact_requiring_tests = _find_resource_node( + component=component, + resource_name='build-result', + ) + assert tee.is_test_required( + artefact_node=artefact_requiring_tests, + extensions_cfg=extensions_cfg, + ) is True + + +def test_test_evidence_collection( + component: ocm.Component, +): + test_evidences = list(tee.iter_test_evidence_resources( + component=component, + )) + assert len(test_evidences) == 1 + + +def test_artefact_test_coverage( + component: ocm.Component, +): + test_evidences = list(tee.iter_test_evidence_resources( + component=component, + )) + + assert tee.has_artefact_test_coverage( + artefact_name='build-result', + test_evidences=test_evidences, + ) == True + + assert tee.has_artefact_test_coverage( + artefact_name='other-build-result', + test_evidences=test_evidences, + ) == False diff --git a/test_evidence_extension.py b/test_evidence_extension.py new file mode 100644 index 00000000..e59ec309 --- /dev/null +++ b/test_evidence_extension.py @@ -0,0 +1,224 @@ +import collections.abc +import datetime +import functools +import logging + +import ci.log +import cnudie.retrieve +import delivery.client +import ocm +import ocm.iter + +import k8s.logging +import k8s.util +import odg.extensions_cfg +import odg.findings +import odg.labels +import odg.model +import odg.util +import paths + + +logger = logging.getLogger(__name__) +ci.log.configure_default_logging() +k8s.logging.configure_kubernetes_logging() +PURPOSE_LABEL_VALUE = 'test' + + +def is_test_required( + artefact_node: ocm.iter.ArtefactNode, + extensions_cfg: odg.extensions_cfg.TestEvidenceConfig, +) -> bool: + artefact = artefact_node.artefact + + # reuse artefact filter once it supports relation attribute + if ( + not extensions_cfg.external_artefacts_require_tests + and artefact_node.artefact.relation is ocm.ResourceRelation.EXTERNAL + ): + return False + + # TODO: use artefact filter once factored out + + test_policy_label = artefact.find_label(name=odg.labels.TestPolicyLabel.name) + if not test_policy_label: + return True # require all resources to provide test evidences by default + + test_policy_label: odg.labels.TestPolicyLabel = odg.labels.deserialise_label( + label=test_policy_label, + ) + return test_policy_label.value + + +def iter_test_evidence_resources( + component: ocm.Component, +) -> collections.abc.Generator[ocm.Resource, None, None]: + return ( + resource + for resource in component.resources + if ( + (label := resource.find_label(name=odg.labels.PurposeLabel.name)) + and PURPOSE_LABEL_VALUE in label.value + ) + ) + + +def has_artefact_test_coverage( + artefact_name: str, + test_evidences: collections.abc.Iterable[ocm.Resource], +) -> bool: + artefact_names_with_test_evidence = [] + for test_evidence in test_evidences: + if not (test_scope_label := test_evidence.find_label(name=odg.labels.TestScopeLabel.name)): + # if label is absent, assume tests are scoping *all* resources within this component + return True + + test_scope_label: ocm.Label + test_scope_label: odg.labels.TestScopeLabel = odg.labels.deserialise_label( + label=test_scope_label, + ) + artefact_names_with_test_evidence.append(test_scope_label.value) + + return artefact_name in artefact_names_with_test_evidence + + +def missing_test_evidence_finding( + artefact: odg.model.ComponentArtefactId, + artefact_node: ocm.iter.ArtefactNode, + sub_type: odg.model.TestStatus, + findings_cfg: odg.findings.Finding, + extensions_cfg: odg.extensions_cfg.TestEvidenceConfig, +) -> odg.model.ArtefactMetadata | None: + categorisation: odg.findings.FindingCategorisation = odg.findings.categorise_finding( + finding_cfg=findings_cfg, + finding_property=sub_type + ) + + if not is_test_required( + artefact_node=artefact_node, + extensions_cfg=extensions_cfg, + ): + return None + + test_evidences: collections.abc.Iterable[ocm.Resource] = iter_test_evidence_resources( + component=artefact_node.component, + ) + + if has_artefact_test_coverage( + artefact_name=artefact_node.artefact.name, + test_evidences=test_evidences, + ): + return None + + now = datetime.datetime.now() + return odg.model.ArtefactMetadata( + artefact=artefact, + meta=odg.model.Metadata( + datasource=odg.model.Datasource.TEST_EVIDENCE, + type=odg.model.Datatype.TEST_EVIDENCE_FINDING, + creation_date=now, + last_update=now, + ), + data=odg.model.TestEvidenceMissingFinding( + test_status=odg.model.TestStatus.NO_TEST_EVIDENCE, + severity=categorisation.id + ), + discovery_date=now.date(), + ) + + +def finding_artefact_metadata( + artefact: odg.model.ComponentArtefactId, + extensions_cfg: odg.extensions_cfg.TestEvidenceConfig, + findings_cfg: odg.findings.Finding, + component_descriptor_lookup: cnudie.retrieve.ComponentDescriptorLookupById, +) -> odg.model.ArtefactMetadata | None: + if not findings_cfg.matches(artefact): + logger.info(f'Findings are filtered out for {artefact=}, skipping...') + return + + artefact_node = k8s.util.get_ocm_node( + component_descriptor_lookup=component_descriptor_lookup, + artefact=artefact, + ) + + if not artefact_node: + logger.info(f'did not find {artefact=}, skipping...') + return + + if not extensions_cfg.is_supported(artefact_kind=artefact.artefact_kind): + if extensions_cfg.on_unsupported is odg.extensions_cfg.WarningVerbosities.FAIL: + raise TypeError(f'{artefact.artefact_kind} is not supported') + return + + return missing_test_evidence_finding( + component_resource_id=artefact, + resource_node=artefact_node, + sub_type=odg.model.TestStatus.NO_TEST_EVIDENCE, + findings_cfg=findings_cfg, + extensions_cfg=extensions_cfg, + ) + + +def scan( + artefact: odg.model.ComponentArtefactId, + extensions_cfg: odg.extensions_cfg.TestEvidenceConfig, + findings_cfg: odg.findings.Finding, + component_descriptor_lookup: cnudie.retrieve.ComponentDescriptorLookupById, + delivery_client: delivery.client.DeliveryServiceClient, + **kwargs, # odg wrapper passes more attributes than we need +): + now = datetime.datetime.now() + + delivery_client.update_metadata( + data=[ + odg.model.ArtefactMetadata( + artefact=artefact, + meta=odg.model.Metadata( + datasource=odg.model.Datasource.TEST_EVIDENCE, + type=odg.model.Datatype.ARTEFACT_SCAN_INFO, + creation_date=now, + last_update=now, + ), + data={}, + discovery_date=now.date(), + ), + finding_artefact_metadata( + artefact=artefact, + extensions_cfg=extensions_cfg, + findings_cfg=findings_cfg, + component_descriptor_lookup=component_descriptor_lookup + ) + ] + ) + + +def main(): + parsed_arguments = odg.util.parse_args() + + if not (findings_cfg_path := parsed_arguments.findings_cfg_path): + findings_cfg_path = paths.findings_cfg_path() + + findings_cfg = odg.findings.Finding.from_file( + path=findings_cfg_path, + finding_type=odg.model.Datatype.TEST_EVIDENCE_FINDING, + ) + + if not findings_cfg: + logger.info('Test evidence findings are disabled, exiting...') + return + + scan_callback = functools.partial( + scan, + findings_cfg=findings_cfg, + ) + + odg.util.process_backlog_items( + parsed_arguments=parsed_arguments, + service=odg.extensions_cfg.Services.TEST_EVIDENCE, + callback=scan_callback, + ) + + +if __name__ == '__main__': + main()