diff --git a/.ocm/base-component.yaml b/.ocm/base-component.yaml index abdf2e1f..9bb640fb 100644 --- a/.ocm/base-component.yaml +++ b/.ocm/base-component.yaml @@ -1,10 +1,10 @@ componentReferences: - componentName: github.com/gardener/cc-utils name: cc-utils - version: 1.2725.0 + version: 1.2730.0 - componentName: ocm.software/ocm-gear/bdba-client name: bdba - version: 0.7.0 + version: 0.8.0 - componentName: ocm.software/ocm-gear/freshclam name: freshclam version: 1.12.0 diff --git a/BDBA_VERSION b/BDBA_VERSION index f8d71478..c70836ca 100644 --- a/BDBA_VERSION +++ b/BDBA_VERSION @@ -1 +1 @@ -0.8.0-dev +0.9.0-dev diff --git a/VERSION b/VERSION index d0b79bc3..bee16995 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1253.0-dev +0.1256.0-dev diff --git a/artefact_enumerator.py b/artefact_enumerator.py index 227674d7..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 @@ -568,7 +585,8 @@ def main(): extensions_cfg = odg.extensions_cfg.ExtensionsConfiguration.from_file(extensions_cfg_path) extensions_cfg: odg.extensions_cfg.ExtensionsConfiguration - logger.info(f'{extensions_cfg.enabled_extensions()=}') + enabled_extensions = list(extensions_cfg.enabled_extensions()) + logger.info(f'{enabled_extensions=}') if not (findings_cfg_path := parsed_arguments.findings_cfg_path): findings_cfg_path = paths.findings_cfg_path() diff --git a/bdba/model.py b/bdba/model.py index 8e2cc71a..867a9cb3 100644 --- a/bdba/model.py +++ b/bdba/model.py @@ -246,6 +246,8 @@ class AnalysisResult(Result): fail_reason: str | None components: list[Component] = dataclasses.field(default_factory=list) custom_data: dict[str, str] = dataclasses.field(default_factory=dict) + binary_bytes: int | None = None + scanned_bytes: int | None = None @dataclasses.dataclass diff --git a/bdba_utils/scan.py b/bdba_utils/scan.py index 1db49ea5..d1abe049 100644 --- a/bdba_utils/scan.py +++ b/bdba_utils/scan.py @@ -211,6 +211,13 @@ def process( logger.error(f'scan of {scanned_element=} failed; {scan_result=}') return + if not (scan_result.binary_bytes and scan_result.scanned_bytes): + logger.error( + f'scan of {scanned_element=} failed: uploaded or scanned file is empty ' + f'({scan_result.binary_bytes=}, {scan_result.scanned_bytes=})' + ) + return + logger.info( f'scan of {scan_result.display_name} succeeded, going to post-process results' ) diff --git a/charts/extensions/values.yaml b/charts/extensions/values.yaml index 2617b786..15015028 100644 --- a/charts/extensions/values.yaml +++ b/charts/extensions/values.yaml @@ -88,3 +88,10 @@ sast: image: repository: null tag: null +test-results: + deployment: + annotations: [] + enabled: false + image: + repository: null + tag: null diff --git a/docs/model.rst b/docs/model.rst index 3cd7078d..0cc5d4d4 100644 --- a/docs/model.rst +++ b/docs/model.rst @@ -236,10 +236,12 @@ Findings (deviations from rulesets) typically have to be processed within an allowed timeframe. Hence, the date of first discovery is stored to allow for the calculation for latest due-dates. Thereby, the initial `discovery_date` must be retained during subsequent updates. Therefore, the `discovery_date` is -part of the `ArtefactMetadata` model. To re-use the initial `discovery_date` of -a finding, and don't reset it as part of every new scan, it must be defined -when a finding is to be interpreted as equal so that the `discovery_date` must -be re-used. +part of the `ArtefactMetadata` model. By default, the initial `discovery_date` +of a finding is re-used in case the OCM identity (except its version and extra +identity) and the `key` property of the finding match. In case it is desired to +deviate from this defalt behaviour (e.g. in case the `key` contains a package +version which should not be considered for the re-use), a custom check must be +implemented as part of the upload metadata route. Considerations ^^^^^^^^^^^^^^ diff --git a/extension-definitions.yaml b/extension-definitions.yaml index 026d188b..6c5364e5 100644 --- a/extension-definitions.yaml +++ b/extension-definitions.yaml @@ -3,13 +3,13 @@ installation: ocm_references: - helm_chart_name: delivery-service name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: delivery-service @@ -87,13 +87,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -119,13 +119,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -144,13 +144,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -170,13 +170,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -196,13 +196,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -222,13 +222,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -254,13 +254,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -286,13 +286,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -317,13 +317,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -349,13 +349,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -381,13 +381,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -413,13 +413,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions @@ -439,13 +439,13 @@ installation: ocm_references: - helm_chart_name: extensions name: ocm.software/ocm-gear/delivery-service - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact: name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" mappings: - name: extensions - version: "0.1253.0-dev" + version: "0.1256.0-dev" artefact_type: helmchart-imagemap value_templates: - helm_chart_name: extensions diff --git a/metadata.py b/metadata.py index 692911f1..c587af46 100644 --- a/metadata.py +++ b/metadata.py @@ -21,16 +21,6 @@ import util -types_with_reusable_discovery_dates = ( - odg.model.Datatype.VULNERABILITY_FINDING, - odg.model.Datatype.LICENSE_FINDING, - odg.model.Datatype.DIKI_FINDING, - odg.model.Datatype.OSID_FINDING, - odg.model.Datatype.CRYPTO_FINDING, - odg.model.Datatype.IP_FINDING, -) - - class ArtefactMetadataQuery(aiohttp.web.View): required_features = (features.FeatureDeliveryDB,) @@ -382,10 +372,8 @@ def find_entry_and_discovery_date( if not found: for existing_entry in existing_entries: if ( - ( - metadata_entry.type not in types_with_reusable_discovery_dates - or discovery_date - ) and metadata_entry.artefact_version not in existing_artefact_versions + discovery_date + and metadata_entry.artefact_version not in existing_artefact_versions ): # there is no need to search any further -> we won't find any existing # entry with the same artefact version and we don't have to find any @@ -510,9 +498,6 @@ def reuse_discovery_date_if_possible( if last_update + reuse_discovery_date.max_reuse_time < datetime.datetime.now(): return None - if new_metadata.type not in types_with_reusable_discovery_dates: - return None - if new_metadata.type == odg.model.Datatype.VULNERABILITY_FINDING: if ( new_metadata.data.get('package_name') == old_metadata.data.get('package_name') @@ -546,16 +531,6 @@ def reuse_discovery_date_if_possible( # resource-/package-version, so we must re-use its discovery date return old_metadata.discovery_date - elif new_metadata.type == odg.model.Datatype.DIKI_FINDING: - if ( - new_metadata.data.get('provider_id') == old_metadata.data.get('provider_id') - and new_metadata.data.get('ruleset_id') == old_metadata.data.get('ruleset_id') - and new_metadata.data.get('rule_id') == old_metadata.data.get('rule_id') - ): - # found the same finding in existing entry, independent of the component-/ - # resource-/ruleset-version, so we must re-use its discovery date - return old_metadata.discovery_date - elif new_metadata.type == odg.model.Datatype.OSID_FINDING: if ( new_metadata.data.get('osid').get('VERSION_ID') @@ -566,16 +541,11 @@ def reuse_discovery_date_if_possible( # found the same version and name in existing entry, so we must re-use its discovery date return old_metadata.discovery_date - elif new_metadata.type == odg.model.Datatype.CRYPTO_FINDING: - if new_metadata.data_key == old_metadata.data_key: - # found the same finding in existing entry, so we must re-use its discovery date - return old_metadata.discovery_date + elif new_metadata.data_key == old_metadata.data_key: + # found the same finding in existing entry, so we must re-use its discovery date + return old_metadata.discovery_date - else: - raise ValueError( - f're-usage of discovery dates is configured for "{new_metadata.type}" but there is no ' - 'special handling implemented to check when to re-use existing dates' - ) + return None def _fill_default_values( diff --git a/odg/extensions_cfg.py b/odg/extensions_cfg.py index 5311549c..9dd6b61f 100644 --- a/odg/extensions_cfg.py +++ b/odg/extensions_cfg.py @@ -39,6 +39,7 @@ class Services(enum.StrEnum): OSID = 'osid' RESPONSIBLES = 'responsibles' SAST = 'sast' + TEST_RESULT_FINDING = 'testResult' ODG_OPERATOR = 'odg-operator' @@ -984,6 +985,13 @@ def is_supported( return is_supported +@dataclasses.dataclass(kw_only=True) +class TestResult(BacklogItemMixins): + def is_supported(self, artefact_kind, access_type): + # do something + return super().is_supported(artefact_kind, access_type) + + @dataclasses.dataclass class ExtensionsConfiguration: access_manager: AccessManagerConfig | None diff --git a/odg/findings.py b/odg/findings.py index c9567d02..4e8cf8a7 100644 --- a/odg/findings.py +++ b/odg/findings.py @@ -134,6 +134,15 @@ class SASTFindingSelector: sub_types: list[str] +@dataclasses.dataclass +class TestResultFindingSelector: + ''' + :param list[str] sub_types: + List of regexes to determine matching missing linter findings. + ''' + status: list[str] + + @dataclasses.dataclass class VulnerabilityFindingSelector: ''' @@ -194,6 +203,7 @@ class FindingCategorisation: | SASTFindingSelector | VulnerabilityFindingSelector | OsIdFindingSelector + | TestResultFindingSelector | None ) @@ -631,6 +641,8 @@ def _validate(self): self._validate_malware() case odg.model.Datatype.SAST_FINDING: self._validate_sast() + case odg.model.Datatype.TEST_RESULT_FINDING: + self._validate_test_result() case odg.model.Datatype.VULNERABILITY_FINDING: self._validate_vulnerabilty() case odg.model.Datatype.INVENTORY_FINDING: @@ -721,6 +733,18 @@ def _validate_malware(self): e.add_note('\n'.join(violations)) raise e + def _validate_test_result(self): + violations = self._validate_categorisations( + expected_selector=TestResultFindingSelector, + ) + + if not violations: + return + + e = ModelValidationError('test result finding violations found:') + e.add_note('\n'.join(violations)) + raise e + def _validate_sast(self): violations = self._validate_categorisations( expected_selector=SASTFindingSelector, @@ -1004,3 +1028,8 @@ def categorise_finding( for status in selector.status: if re.fullmatch(status, finding_property, re.IGNORECASE): return categorisation + + elif isinstance(selector, TestResultFindingSelector): + 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..3bc314f6 100644 --- a/odg/findings_cfg.yaml +++ b/odg/findings_cfg.yaml @@ -46,6 +46,24 @@ malware_names: - .* +- type: finding/testresults + issues: + enable_assignees: False # should be true? + categorisations: + - id: NONE + display_name: NONE + value: 0 + allowed_processing_time: ~ + #rescoring: manual # do we need this? + - id: BLOCKER + display_name: BLOCKER + value: 16 + allowed_processing_time: 0 + #rescoring: manual # do we need this? + selector: + sub_types: + - .* + - type: finding/sast issues: enable_issues: False diff --git a/odg/labels.py b/odg/labels.py index a011a9a4..b178580f 100644 --- a/odg/labels.py +++ b/odg/labels.py @@ -40,6 +40,19 @@ class ScanningHint(LabelValue): comment: str | None +@dataclasses.dataclass(frozen=True) +class TestPolicy(Label): + name = 'gardener.cloud/test-policy' + value: tuple[str, ...] + + +@dataclasses.dataclass(frozen=True) +class TestHint(LabelValue): # do I need this? + policy: TestPolicy + path_config: PathRegexes | None + comment: str | None + + @dataclasses.dataclass(frozen=True) class BinaryIdScanLabel(Label): name = 'cloud.gardener.cnudie/dso/scanning-hints/binary_id/v1' @@ -52,12 +65,25 @@ class SourceScanLabel(Label): value: ScanningHint +@dataclasses.dataclass(frozen=True) +class TestResultLabel(Label): + # where do I get this from? + name = 'cloud.gardener/testresults' + value: TestHint # do I need this? + + @dataclasses.dataclass(frozen=True) class PurposeLabel(Label): name = 'gardener.cloud/purposes' value: tuple[str, ...] +@dataclasses.dataclass(frozen=True) +class TestScope(Label): + name = 'gardener.cloud/test-scope' + value: bool + + @dataclasses.dataclass(frozen=True) class PackageVersionHint: name: str diff --git a/odg/model.py b/odg/model.py index 83ec245e..51ac9961 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_RESULT_FINDING = 'finding/testresults' # 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_RESULT_FINDING: Datasource.TEST_RESULT_FINDING }[self] @@ -80,6 +82,7 @@ class Datasource(enum.StrEnum): OSID = 'osid' RESPONSIBLES = 'responsibles' SAST = 'sast' + TEST_RESULT_FINDING = 'test-result' def datatypes(self) -> tuple[Datatype, ...]: return { @@ -123,6 +126,9 @@ def datatypes(self) -> tuple[Datatype, ...]: Datasource.SAST: ( Datatype.SAST_FINDING, ), + Datasource.TEST_RESULT_FINDING: ( + Datatype.TEST_RESULT_FINDING + ) }.get(self, tuple()) @@ -174,13 +180,18 @@ 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 = 'no-test' + + class SastSubType(enum.StrEnum): LOCAL_LINTING = 'local-linting' CENTRAL_LINTING = 'central-linting' @@ -470,6 +481,15 @@ def key(self) -> str: return _as_key(self.package_name, self.package_version, self.license.name) +@dataclasses.dataclass +class TestResultMissingFinding(Finding): + test_status: TestStatus + + @property + def key(self) -> str: + return _as_key(self.test_status) + + @dataclasses.dataclass class VulnerabilityFinding(Finding, BDBAMixin): cve: str @@ -1253,7 +1273,8 @@ class KyvernoRuleResult: skip: int = 0 error: int = 0 warn: int = 0 - violations: typing.List[KyvernoViolation] = dataclasses.field(default_factory=list) + violations: typing.List[KyvernoViolation] = dataclasses.field( + default_factory=list) @dataclasses.dataclass @@ -1275,7 +1296,8 @@ class KyvernoReportSummary: dataclasses.field(default_factory=dict) # namespace -> NamespaceSummary ) policy_results: dict[str, dict[str, dict[str, KyvernoRuleResult]]] = ( - dataclasses.field(default_factory=dict) # namespace -> policy -> rule -> RuleResult + # namespace -> policy -> rule -> RuleResult + dataclasses.field(default_factory=dict) ) @@ -1423,6 +1445,7 @@ def key(self) -> str: | OsIdFinding | SastFinding | VulnerabilityFinding + | TestResultMissingFinding ) InformationalModels = ( StructureInfo @@ -1455,7 +1478,8 @@ class ArtefactMetadata: artefact: ComponentArtefactId meta: Metadata data: FindingModels | InformationalModels | MetaModels - discovery_date: datetime.date | None = None # required for finding specific SLA tracking + # required for finding specific SLA tracking + discovery_date: datetime.date | None = None allowed_processing_time: str | None = None @staticmethod diff --git a/odg/profiles.yaml b/odg/profiles.yaml index 8d241e13..7e9389dd 100644 --- a/odg/profiles.yaml +++ b/odg/profiles.yaml @@ -10,7 +10,13 @@ - finding/license - finding/malware - finding/osid + - finding/ghas + - finding/test-result - finding/sast - - finding/vulnerability + - finding/diki + - finding/crypto + - finding/osid + - finding/ghas + - finding/test-result special_component_ids: - 03e237b4-434e-4e1c-b786-5ceb6cc76c1c diff --git a/requirements.extensions.txt b/requirements.extensions.txt index 24f0ff43..629cfedb 100644 --- a/requirements.extensions.txt +++ b/requirements.extensions.txt @@ -1,3 +1,3 @@ -bdba==0.7.0 +bdba==0.8.0 cyclonedx-python-lib jsonpath-ng diff --git a/requirements.utils.txt b/requirements.utils.txt index 1a4069bb..927f13fb 100644 --- a/requirements.utils.txt +++ b/requirements.utils.txt @@ -2,7 +2,7 @@ aiohttp boto3 cachetools dacite -gardener-cicd-libs==1.2725.0 +gardener-cicd-libs==1.2730.0 github3.py kubernetes requests diff --git a/test/resources/missing_test_result_finding.yaml b/test/resources/missing_test_result_finding.yaml new file mode 100644 index 00000000..2447c00d --- /dev/null +++ b/test/resources/missing_test_result_finding.yaml @@ -0,0 +1,17 @@ +categorisations: + - id: NONE + display_name: test exists and has no findings + value: 0 + allowed_processing_time: 0 + rescoring: null + selector: null + + - id: BLOCKER + display_name: Test result is missing + value: 16 + allowed_processing_time: 0 + rescoring: null + selector: + type: finding/testresults + status: + - ".*" \ No newline at end of file diff --git a/test/resources/missing_test_results_component_descriptor.yaml b/test/resources/missing_test_results_component_descriptor.yaml new file mode 100644 index 00000000..816d3291 --- /dev/null +++ b/test/resources/missing_test_results_component_descriptor.yaml @@ -0,0 +1,86 @@ +component: + name: ThisIsMyComponent + version: v1.2710.0 + repositoryContexts: + - baseUrl: foo + subPath: None + type: ociRegistry + provider: Any + sources: [] + componentReferences: [] + resources: + - access: + localReference: foo + mediaType: application/data + referenceName: None + type: localBlob/v1s + labels: + - name: gardener.cloud/test-policy + value: false + - name: gardener.cloud/purposes + signing: false + value: + - test + version: None + - name: gardener.cloud/test-scope + value: + - job-image-1 + - job-image-2 + name: job-image-test-1 + relation: local + type: application/gzip + version: v1.2710.0 + - access: + localReference: bar + mediaType: application/data + referenceName: None + type: localBlob/v1 + labels: + - name: gardener.cloud/test-policy + value: false + - name: gardener.cloud/purposes + signing: false + value: + - test + version: None + - name: gardener.cloud/test-scope + value: + - job-image-2 + name: job-image-test-2 + relation: local + type: application/gzip + version: v1.2710.0 + - access: + localReference: foo + type: ociRegistry + labels: + - name: gardener.cloud/test-policy + value: true + name: job-image-1 + relation: local + type: ociImage + version: v1.2710.0 + - access: + localReference: bar + type: ociRegistry + labels: + - name: gardener.cloud/test-policy + value: true + name: job-image-2 + relation: local + type: ociImage + version: v1.2710.0 + - access: + localReference: bar + type: ociRegistry + labels: + - name: gardener.cloud/test-policy + value: false + name: job-image-3 + relation: local + type: helmChart/v1 + version: v1.2710.0 + labels: [] +meta: + schemaVersion: v2 + diff --git a/test/test_odg_extension_test_results.py b/test/test_odg_extension_test_results.py new file mode 100644 index 00000000..3124eb3a --- /dev/null +++ b/test/test_odg_extension_test_results.py @@ -0,0 +1,120 @@ +import pytest +import dacite +import enum +import ocm +import odg.model +import odg.findings +import test_results +import yaml +from pathlib import Path + + +@pytest.fixture +def component_descriptor() -> ocm.ComponentDescriptor: + yaml_path = Path(__file__).parent / "resources" / \ + "missing_test_results_component_descriptor.yaml" + + raw = yaml.safe_load(yaml_path.read_text()) + + return dacite.from_dict( + data=raw, + data_class=ocm.ComponentDescriptor, + config=dacite.Config(cast=[enum.Enum]) + ) + + +@pytest.fixture +def missing_test_result_finding_cfg() -> odg.findings.Finding: + yaml_path_finding = Path(__file__).parent / \ + "resources" / "missing_test_result_finding.yaml" + + raw = yaml.safe_load(yaml_path_finding.read_text()) + + categorisations = [] + + for cat in raw["categorisations"]: + selector = None + + if cat["selector"] is not None: + selector = odg.findings.TestResultFindingSelector( + status=cat["selector"]["status"] + ) + + categorisations.append( + odg.findings.FindingCategorisation( + id=cat["id"], + display_name=cat["display_name"], + value=cat["value"], + allowed_processing_time=cat["allowed_processing_time"], + rescoring=cat["rescoring"], + selector=selector + ) + ) + + return odg.findings.Finding( + type=odg.model.Datatype.TEST_RESULT_FINDING, + filter=None, + rescoring_ruleset=None, + categorisations=categorisations + ) + + +def test_artefact_test_results_filter( + component_descriptor: ocm.ComponentDescriptor, + missing_test_result_finding_cfg: odg.findings.Finding +): + + resources_req_tests = list(test_results.iter_artefacts_requiring_tests( + component=component_descriptor)) + + test_resources = list(test_results.find_test_artefacts( + component=component_descriptor)) + + assert len(resources_req_tests) == len(test_resources) + + # mismatching test scope + test_scope_values = component_descriptor.component.resources[0].labels[2].value + + test_scope_values.remove("job-image-1") + + test_resources = test_results.find_test_artefacts( + component=component_descriptor) + + test_coverage = test_results.iter_artefacts_for_test_coverage( + test_result_finding_config=missing_test_result_finding_cfg, + component=component_descriptor, + artefact=odg.model.ComponentArtefactId, + sub_type=odg.model.TestStatus.NO_TEST) + + assert len(test_coverage) == 1 + +# no test scope + + labels = component_descriptor.component.resources[0].labels + labels = [label for label in labels if not label.name == + 'gardener.cloud/test-scope'] + + component_descriptor.component.resources[0].labels = labels + + test_resources = test_results.find_test_artefacts( + component=component_descriptor) + + test_coverage = test_results.iter_artefacts_for_test_coverage( + component=component_descriptor, + artefact=odg.model.ComponentArtefactId, + test_result_finding_config=missing_test_result_finding_cfg, + sub_type=odg.model.TestStatus.NO_TEST) + + assert len(test_coverage) == 0 + +# let's remove the test results + component_descriptor.component.resources = [ + resource + for resource in component_descriptor.component.resources + if not resource.name == 'job-image-test-2' and not resource.name == 'job-image-test-1' + ] + + test_resources = list(test_results.find_test_artefacts( + component=component_descriptor)) + + assert len(resources_req_tests) != len(test_resources) diff --git a/test_results.py b/test_results.py new file mode 100644 index 00000000..2aafec72 --- /dev/null +++ b/test_results.py @@ -0,0 +1,90 @@ +import datetime +import ocm +import odg.findings +import odg.model + + +def iter_artefacts_requiring_tests(component: ocm.ComponentDescriptor) -> list[ocm.Artifact] | None: + for resource in component.component.resources: + if resource.relation == 'local': # !!WE NEED TO DISCUSS THIS IN THE TEAM! + label = resource.find_label(name='gardener.cloud/test-policy') + if label and label.value: + yield resource + else: + if resource.type == ocm.ArtefactType.OCI_IMAGE: + yield resource + else: + continue + + +PURPOSE_LABEL_NAME = 'gardener.cloud/purposes' +PURPOSE_LABEL_VALUE = 'test' + + +def find_test_artefacts(component: ocm.ComponentDescriptor) -> list[ocm.Artifact]: + for resource in component.component.resources: + for label in resource.labels: + if label.name == PURPOSE_LABEL_NAME and PURPOSE_LABEL_VALUEs in label.value: + yield resource + + +def create_missing_test_finding( + artefact: odg.model.ComponentArtefactId, + sub_type: odg.model.TestStatus, + categorisation: odg.findings.FindingCategorisation, + creation_timestamp: datetime.datetime=datetime.datetime.now( + tz=datetime.timezone.utc) +) -> odg.model.ArtefactMetadata | None: + return odg.model.ArtefactMetadata( + artefact=artefact, + meta=odg.model.Metadata( + datasource=odg.model.Datasource.TEST_RESULT_FINDING, + type=odg.model.Datatype.TEST_RESULT_FINDING, + datasource=odg.model.Datasource.TEST_RESULT_FINDING, + type=odg.model.Datatype.TEST_RESULT_FINDING, + creation_date=creation_timestamp, + last_update=creation_timestamp, + ), + data=odg.model.TestResultMissingFinding( + test_status=odg.model.TestStatus.NO_TEST, + severity=categorisation.id), + discovery_date=creation_timestamp.date(), + ) + + +def iter_artefacts_for_test_coverage( + component: ocm.ComponentDescriptor, + test_result_finding_config: odg.findings.Finding, + sub_type: odg.model.TestStatus, + artefact: odg.model.ComponentArtefactId, + creation_timestamp: datetime.datetime=datetime.datetime.now( + datetime.timezone.utc) +) -> odg.model.ComponentArtefactId | odg.findings.Finding: + + categorisation = odg.findings.categorise_finding( + finding_cfg=test_result_finding_config, + finding_property=sub_type + ) + + artefacts_req_tests = iter_artefacts_requiring_tests(component) + + test_artefacts = find_test_artefacts(component) + + findings = [] + artefacts_with_tests = [] + + for ta in test_artefacts: + if not ta.find_label(name='gardener.cloud/test-scope'): + # if label is absent, assume tests are scoping *all* resources within this component + return [] + for label in ta.labels: + if label.name == 'gardener.cloud/test-scope': + artefacts_with_tests.append(label.value) + + for artefact_requiring_tests in artefacts_req_tests: + if artefact_requiring_tests.name not in artefacts_with_tests: + findings.append( + create_missing_test_finding( + artefact, sub_type, categorisation, creation_timestamp) + ) + return findings