Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a timestamp validation check #850

Open
wants to merge 5 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions src/macaron/slsa_analyzer/checks/timestamp_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.

"""This module implements a check to verify the timestamp difference between commit finder and the latest version in Maven."""

import logging
from datetime import datetime
from datetime import timedelta

from sqlalchemy import ForeignKey, String, Interval
from sqlalchemy.orm import Mapped, mapped_column

from macaron.database.table_definitions import CheckFacts
from macaron.database.db_custom_types import RFC3339DateTime
from macaron.errors import InvalidHTTPResponseError
from macaron.slsa_analyzer.analyze_context import AnalyzeContext
from macaron.slsa_analyzer.build_tool.maven import Maven
from macaron.slsa_analyzer.checks.base_check import BaseCheck
from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType
from macaron.slsa_analyzer.package_registry.maven_central_registry import MavenCentralRegistry
from macaron.slsa_analyzer.registry import registry
from macaron.slsa_analyzer.slsa_req import ReqName
from macaron.slsa_analyzer.specs.package_registry_spec import PackageRegistryInfo

logger: logging.Logger = logging.getLogger(__name__)


class TimestampCheckFacts(CheckFacts):
"""The ORM mapping for justifications in timestamp check."""

__tablename__ = "_timestamp_check"

# The primary key.
id: Mapped[int] = mapped_column(ForeignKey("_check_facts.id"), primary_key=True) # noqa: A003

#: The package name.
package_name: Mapped[str] = mapped_column(String, nullable=False)

#: The commit finder date.
commit_finder_date: Mapped[datetime] = mapped_column(RFC3339DateTime, nullable=False)

#: The latest timestamp from Maven.
latest_timestamp: Mapped[datetime] = mapped_column(RFC3339DateTime, nullable=False)

#: The time difference.
time_difference: Mapped[Interval] = mapped_column(Interval, nullable=False, info={"justification": JustificationType.TEXT})

#: The latest version.
latest_version: Mapped[str] = mapped_column(String, nullable=False)

__mapper_args__ = {
"polymorphic_identity": "_timestamp_check",
}


class TimestampCheck(BaseCheck):
"""This Check verifies the timestamp difference between commit finder and the latest version in Maven."""

def __init__(self) -> None:
"""Initialize instance."""
check_id = "mcn_timestamp_check_1"
description = "Check timestamp difference between commit finder and latest version in Maven."
depends_on: list[tuple[str, CheckResultType]] = []
eval_reqs = [ReqName.VCS]
super().__init__(check_id=check_id, description=description, depends_on=depends_on, eval_reqs=eval_reqs)

def run_check(self, ctx: AnalyzeContext) -> CheckResultData:
"""Implement the check in this method.

Parameters
----------
ctx : AnalyzeContext
The object containing processed data for the target repo.

Returns
-------
CheckResultData
The result type of the check.
"""
# Get the commit date from Macaron's commit finder
commit_finder_date = ctx.component.repository.commit_date
if not commit_finder_date:
logger.info("No commit date found for the component.")
return CheckResultData(result_tables=[], result_type=CheckResultType.FAILED)

# Look for the artifact in the corresponding registry and find the publish timestamp.
artifact_published_date = None
package_registry_info_entries = ctx.dynamic_data["package_registries"]
for package_registry_info_entry in package_registry_info_entries:
match package_registry_info_entry:
case PackageRegistryInfo(
build_tool=Maven(),
package_registry=MavenCentralRegistry() as mvn_central_registry,
):
group_id = ctx.component.namespace
artifact_id = ctx.component.name
version = ctx.component.version
try:
artifact_published_date = mvn_central_registry.find_publish_timestamp(
group_id, artifact_id, version
)
except InvalidHTTPResponseError as error:
logger.debug(error)

if not artifact_published_date:
return CheckResultData(result_tables=[], result_type=CheckResultType.FAILED)

# Compare timestamps
time_difference = artifact_published_date - commit_finder_date
package_name = f"{ctx.component.namespace}/{ctx.component.name}"

result_facts = TimestampCheckFacts(
package_name=package_name,
commit_finder_date=commit_finder_date,
latest_timestamp=artifact_published_date,
time_difference=time_difference,
latest_version=ctx.component.version,
confidence=Confidence.HIGH
)

if time_difference > timedelta(hours=24):
return CheckResultData(result_tables=[result_facts], result_type=CheckResultType.PASSED)
else:
return CheckResultData(result_tables=[], result_type=CheckResultType.FAILED)


registry.register(TimestampCheck())
46 changes: 46 additions & 0 deletions tests/slsa_analyzer/checks/test_timestamp_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.

"""This module contains tests for the Timestamp Check."""
from datetime import datetime
from datetime import timedelta
from pathlib import Path

import pytest
from pytest_httpserver import HTTPServer

from macaron.database.table_definitions import Repository
from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType
from macaron.slsa_analyzer.checks.check_result import CheckResultType
from macaron.slsa_analyzer.checks.timestamp_check import TimestampCheck
from macaron.slsa_analyzer.specs.package_registry_spec import PackageRegistryInfo
from macaron.database.db_custom_types import RFC3339DateTime
from tests.conftest import MockAnalyzeContext

@pytest.mark.parametrize(
("repository", "package_registry_info_entries", "expected"),
[
(None, [], CheckResultType.FAILED),
(Repository(complete_name="github.com/package-url/purl-spec", commit_date=RFC3339DateTime()), [], CheckResultType.FAILED),
(Repository(complete_name="github.com/package-url/purl-spec", commit_date=RFC3339DateTime()), [{"build_tool": "Maven", "package_registry": "MavenCentralRegistry"}], CheckResultType.FAILED),
(Repository(complete_name="github.com/package-url/purl-spec", commit_date=RFC3339DateTime() - timedelta(days=2)), [{"build_tool": "Maven", "package_registry": "MavenCentralRegistry", "published_date": RFC3339DateTime() - timedelta(hours=25)}], CheckResultType.PASSED),
],
)
def test_timestamp_check(httpserver: HTTPServer, macaron_path: Path, repository: Repository, package_registry_info_entries: list, expected: str) -> None:
"""Test that the check handles repositories and package registry info correctly."""
check = TimestampCheck()

# Set up the context object with dynamic data and repository.
ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="")
ctx.component.repository = repository
ctx.dynamic_data["package_registries"] = package_registry_info_entries

# Mock the find_publish_timestamp method for MavenCentralRegistry using the httpserver
httpserver.expect_request("/maven-central-timestamp").respond_with_json({"published_date": "2024-08-29T12:00:00Z"})

# Replace the MavenCentralRegistry with the mock
for entry in package_registry_info_entries:
if entry["package_registry"] == "MavenCentralRegistry":
entry["package_registry"] = httpserver.url_for("/maven-central-timestamp")

assert check.run_check(ctx).result_type == expected