diff --git a/README.md b/README.md index 925f6e127..4493efb1e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Use Macaron as a GitHub Action To use the Macaron GitHub Action, add the following step to your workflow (adjust the version as needed). In this example, we use an example policy. For detailed instructions and a comprehensive list of available options, please refer to the [Macaron GitHub Action documentation](https://oracle.github.io/macaron/pages/macaron_action.html). ```yaml -- uses: oracle/macaron@v0.21.0 +- uses: oracle/macaron@v0.22.0 with: repo_path: 'https://github.com/example/project' policy_file: check-github-actions diff --git a/action.yaml b/action.yaml index f28f9d2e9..418f37705 100644 --- a/action.yaml +++ b/action.yaml @@ -1,9 +1,12 @@ -# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2025 - 2026, 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/. -name: Macaron Security Analysis -description: Run Macaron to analyze supply chain security +name: Macaron Security Analysis Action +description: Run Macaron to analyze artifacts for supply chain security author: Oracle - github.com/oracle/macaron +branding: + icon: shield + color: blue # This composite GitHub Action wraps the Macaron tool. It exposes inputs for analysis options to shell scripts under `scripts/actions/` for readability. inputs: diff --git a/docs/source/pages/cli_usage/command_gen_build_spec.rst b/docs/source/pages/cli_usage/command_gen_build_spec.rst index c4112e0ab..a310fe15d 100644 --- a/docs/source/pages/cli_usage/command_gen_build_spec.rst +++ b/docs/source/pages/cli_usage/command_gen_build_spec.rst @@ -40,3 +40,11 @@ Options .. option:: --output-format OUTPUT_FORMAT The output format. Can be `default-buildspec` (default), `rc-buildspec` (Reproducible-central build spec for Java), or `dockerfile` (currently only supported for Python packages) + +.. _gen-build-spec-schema: + +-------------------------- +Build Specification Schema +-------------------------- + +The corresponding JSON schema is available in the `resources directory `_ of the repository. Be sure to use the schema that matches your Macaron release by selecting the appropriate GitHub tag. diff --git a/docs/source/pages/macaron_action.rst b/docs/source/pages/macaron_action.rst index 6c7db9407..dc8ebb477 100644 --- a/docs/source/pages/macaron_action.rst +++ b/docs/source/pages/macaron_action.rst @@ -18,8 +18,8 @@ When using this action you can reference the action in your workflow. Example: runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Run Macaron Security Analysis - uses: oracle/macaron@v0.21.0 + - name: Run Macaron Security Analysis Action + uses: oracle/macaron@v0.22.0 with: repo_path: 'https://github.com/example/project' policy_file: check-github-actions @@ -37,7 +37,7 @@ directory containing ``macaron.db``: .. code-block:: yaml - name: Verify policy - uses: oracle/macaron@v0.21.0 + uses: oracle/macaron@v0.22.0 with: policy_file: policy.dl output_dir: macaron-output diff --git a/docs/source/pages/tutorials/rebuild_third_party_artifacts.rst b/docs/source/pages/tutorials/rebuild_third_party_artifacts.rst index 24bb9fd22..d0ccf24a0 100644 --- a/docs/source/pages/tutorials/rebuild_third_party_artifacts.rst +++ b/docs/source/pages/tutorials/rebuild_third_party_artifacts.rst @@ -106,7 +106,7 @@ In the example above, the buildspec is located at: Step 3: Review and Use the Buildspec File ========================================= -By default we generate the buildspec in JSON format as follows: +By default we generate the buildspec in JSON format as follows (see more details about the schema :ref:`here `): .. code-block:: ini diff --git a/src/macaron/build_spec_generator/common_spec/base_spec.py b/src/macaron/build_spec_generator/common_spec/base_spec.py index 6477801fd..ac954c0a3 100644 --- a/src/macaron/build_spec_generator/common_spec/base_spec.py +++ b/src/macaron/build_spec_generator/common_spec/base_spec.py @@ -85,7 +85,7 @@ class BaseBuildSpecDict(TypedDict, total=False): has_binaries: NotRequired[bool] #: The artifacts that were analyzed in generating the build specification. - upstream_artifacts: dict[str, list[str]] + upstream_artifacts: NotRequired[dict[str, list[str]]] class BaseBuildSpec(ABC): diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/closer_release_join_date.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/closer_release_join_date.py index bfa9a0704..47f958dfb 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/closer_release_join_date.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/closer_release_join_date.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2026, 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/. """Analyzer checks whether the maintainers' join date closer to latest package's release date.""" @@ -10,7 +10,7 @@ from macaron.malware_analyzer.datetime_parser import parse_datetime from macaron.malware_analyzer.pypi_heuristics.base_analyzer import BaseHeuristicAnalyzer from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult, Heuristics -from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset, PyPIRegistry +from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset class CloserReleaseJoinDateAnalyzer(BaseHeuristicAnalyzer): @@ -33,30 +33,28 @@ def _load_defaults(self) -> int: return section.getint("timedelta_threshold_of_join_release", 5) return 5 - def _get_maintainers_join_date(self, pypi_registry: PyPIRegistry, package_name: str) -> list[datetime] | None: + def _get_maintainers_join_date(self, pypi_package_json: PyPIPackageJsonAsset) -> list[datetime] | None: """Get the join date of the maintainers. Each package might have multiple maintainers. Parameters ---------- - pypi_registry: PyPIRegistry - The PyPI registry implementation. - package_name: str - The package name. + pypi_package_json: PyPIPackageJsonAsset + The PyPI package JSON asset object. Returns ------- list[datetime] | None The maintainers' join date. """ - maintainers: list | None = pypi_registry.get_maintainers_of_package(package_name) + maintainers: list | None = pypi_package_json.get_maintainers_of_package() if maintainers is None: return None join_dates: list[datetime] = [] for maintainer in maintainers: - maintainer_join_date = pypi_registry.get_maintainer_join_date(maintainer) + maintainer_join_date = pypi_package_json.pypi_registry.get_maintainer_join_date(maintainer) if maintainer_join_date is not None: join_dates.append(maintainer_join_date) @@ -94,9 +92,7 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes tuple[HeuristicResult, dict[str, JsonType]]: The result and related information collected during the analysis. """ - maintainers_join_date: list[datetime] | None = self._get_maintainers_join_date( - pypi_package_json.pypi_registry, pypi_package_json.component_name - ) + maintainers_join_date: list[datetime] | None = self._get_maintainers_join_date(pypi_package_json) latest_release_date: datetime | None = self._get_latest_release_date(pypi_package_json) detail_info: dict[str, JsonType] = { "maintainers_join_date": ( diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/similar_projects.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/similar_projects.py index b98686c99..aa9c2dde1 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/similar_projects.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/similar_projects.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2026, 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 analyzer checks if the package has a similar structure to other packages maintained by the same user.""" @@ -50,7 +50,7 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes similar_projects: list[str] = [] result: HeuristicResult = HeuristicResult.PASS - maintainers = pypi_package_json.pypi_registry.get_maintainers_of_package(pypi_package_json.component_name) + maintainers = pypi_package_json.get_maintainers_of_package() if not maintainers: # NOTE: This would ideally raise an error, identifying malformed package information, but issues with # obtaining maintainer information from the HTML page means this will remains as a SKIP for now. diff --git a/src/macaron/resources/schemastore/find_source_report_schema.json b/src/macaron/resources/schemas/find_source_report_schema.json similarity index 100% rename from src/macaron/resources/schemastore/find_source_report_schema.json rename to src/macaron/resources/schemas/find_source_report_schema.json diff --git a/src/macaron/resources/schemas/macaron_buildspec_schema.json b/src/macaron/resources/schemas/macaron_buildspec_schema.json new file mode 100644 index 000000000..f07921b34 --- /dev/null +++ b/src/macaron/resources/schemas/macaron_buildspec_schema.json @@ -0,0 +1,130 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Macaron BuildSpec Schema", + "type": "object", + "description": "Schema for build specification supporting multiple languages, build tools, and metadata.", + "properties": { + "ecosystem": { + "type": "string", + "description": "The package ecosystem." + }, + "purl": { + "type": "string", + "description": "The PackageURL identifier." + }, + "language": { + "type": "string", + "description": "The programming language, e.g., 'java', 'python', 'javascript'." + }, + "build_tools": { + "type": "array", + "items": { "type": "string" }, + "description": "The build tools or package managers, e.g., 'maven', 'gradle', 'pip', etc." + }, + "macaron_version": { + "type": "string", + "description": "The version of Macaron used for generating the spec." + }, + "group_id": { + "type": ["string", "null"], + "description": "The group identifier for the project/component." + }, + "artifact_id": { + "type": "string", + "description": "The artifact identifier for the project/component." + }, + "version": { + "type": "string", + "description": "The version of the package or component." + }, + "git_repo": { + "type": "string", + "description": "The remote path or URL of the git repository." + }, + "git_tag": { + "type": "string", + "description": "The commit SHA or tag in the VCS repository." + }, + "newline": { + "type": "string", + "description": "The type of line endings used (e.g., 'lf', 'crlf')." + }, + "language_version": { + "type": "array", + "items": { "type": "string" }, + "description": "The version(s) of the programming language or runtime." + }, + "dependencies": { + "type": "array", + "items": { "type": "string" }, + "description": "List of release dependencies." + }, + "build_dependencies": { + "type": "array", + "items": { "type": "string" }, + "description": "List of build dependencies, which includes tests." + }, + "build_commands": { + "type": "array", + "items": { + "type": "array", + "items": { "type": "string" } + }, + "description": "List of shell commands to build the project." + }, + "test_commands": { + "type": "array", + "items": { + "type": "array", + "items": { "type": "string" } + }, + "description": "List of shell commands to test the project." + }, + "environment": { + "type": "object", + "additionalProperties": { "type": "string" }, + "description": "Environment variables required during build or test." + }, + "artifact_path": { + "type": ["string", "null"], + "description": "Path or location of the build artifact/output." + }, + "entry_point": { + "type": ["string", "null"], + "description": "Entry point script, class, or binary for running the project." + }, + "build_requires": { + "type": "object", + "additionalProperties": { "type": "string" }, + "description": "Required packages that must be available in the build environment." + }, + "build_backends": { + "type": "array", + "items": { "type": "string" }, + "description": "List of build back-end tools used." + }, + "has_binaries": { + "type": "boolean", + "description": "Flag to indicate if the artifact includes binaries." + }, + "upstream_artifacts": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { "type": "string" } + }, + "description": "The artifacts that were analyzed in generating the build specification." + } + }, + "required": [ + "ecosystem", + "purl", + "language", + "build_tools", + "macaron_version", + "artifact_id", + "version", + "language_version" + ], + "additionalProperties": false +} diff --git a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py index bce197890..432d14aa7 100644 --- a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py +++ b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py @@ -397,26 +397,6 @@ def get_package_page(self, package_name: str) -> str | None: return html_snippets return None - def get_maintainers_of_package(self, package_name: str) -> list | None: - """Implement custom API to get all maintainers of the package. - - Parameters - ---------- - package_name: str - The package name. - - Returns - ------- - list | None - The list of maintainers. - """ - package_page: str | None = self.get_package_page(package_name) - if package_page is None: - return None - soup = BeautifulSoup(package_page, "html.parser") - maintainers = soup.find_all("span", class_="sidebar-section__user-gravatar-text") - return list({maintainer.get_text(strip=True) for maintainer in maintainers}) - def get_maintainer_profile_page(self, username: str) -> str | None: """Implement custom API to get maintainer's profile page. @@ -772,6 +752,25 @@ def get_releases(self) -> dict | None: """ return json_extract(self.package_json, ["releases"], dict) + def get_maintainers_of_package(self) -> list | None: + """Return the names of all maintainers of this package. + + Returns + ------- + list | None + The list of maintainers. + """ + maintainers: list[str] = [] + maintainer_roles = json_extract(self.package_json, ["ownership", "roles"], list) + if maintainer_roles is None: + return None + + for maintainer_with_role in maintainer_roles: + if (maintainer := maintainer_with_role.get("user", None)) is not None: + maintainers.append(maintainer) + + return maintainers + def get_project_links(self) -> dict | None: """Retrieve the project links from the base metadata. diff --git a/tests/integration/cases/org_apache_hugegraph/computer-k8s/test.yaml b/tests/integration/cases/org_apache_hugegraph/computer-k8s/test.yaml index 1a4b5f034..6cfbc8864 100644 --- a/tests/integration/cases/org_apache_hugegraph/computer-k8s/test.yaml +++ b/tests/integration/cases/org_apache_hugegraph/computer-k8s/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2025 - 2026, 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/. description: | @@ -42,3 +42,9 @@ steps: kind: default_build_spec result: output/buildspec/maven/org_apache_hugegraph/computer-k8s/macaron.buildspec expected: expected_default.buildspec +- name: Validate the produced buildspec + kind: validate_schema + options: + kind: json_schema + schema: macaron_buildspec_json + result: output/buildspec/maven/org_apache_hugegraph/computer-k8s/macaron.buildspec diff --git a/tests/integration/cases/pypi_toga/test.yaml b/tests/integration/cases/pypi_toga/test.yaml index 1118f3217..2a6226ece 100644 --- a/tests/integration/cases/pypi_toga/test.yaml +++ b/tests/integration/cases/pypi_toga/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2025 - 2026, 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/. description: | @@ -48,3 +48,9 @@ steps: kind: dockerfile_build_spec result: output/buildspec/pypi/toga/dockerfile.buildspec expected: expected_dockerfile.buildspec +- name: Validate the produced buildspec + kind: validate_schema + options: + kind: json_schema + schema: macaron_buildspec_json + result: output/buildspec/pypi/toga/macaron.buildspec diff --git a/tests/integration/run.py b/tests/integration/run.py index 45d7ed93a..dd79bd04a 100644 --- a/tests/integration/run.py +++ b/tests/integration/run.py @@ -91,7 +91,8 @@ def configure_logging(verbose: bool) -> None: DEFAULT_SCHEMAS: dict[str, Sequence[str]] = { "output_json_report": ["tests", "schema_validation", "report_schema.json"], - "find_source_json_report": ["src", "macaron", "resources", "schemastore", "find_source_report_schema.json"], + "find_source_json_report": ["src", "macaron", "resources", "schemas", "find_source_report_schema.json"], + "macaron_buildspec_json": ["src", "macaron", "resources", "schemas", "macaron_buildspec_schema.json"], } diff --git a/tests/malware_analyzer/pypi/test_closer_release_join_date.py b/tests/malware_analyzer/pypi/test_closer_release_join_date.py index 309574a21..5eb131300 100644 --- a/tests/malware_analyzer/pypi/test_closer_release_join_date.py +++ b/tests/malware_analyzer/pypi/test_closer_release_join_date.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2026, 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/. """Tests for closer release join date heuristic.""" @@ -14,7 +14,7 @@ def test_analyze_pass(pypi_package_json: MagicMock) -> None: analyzer = CloserReleaseJoinDateAnalyzer() # Set up mock return values. - pypi_package_json.pypi_registry.get_maintainers_of_package.return_value = ["maintainer1", "maintainer2"] + pypi_package_json.get_maintainers_of_package.return_value = ["maintainer1", "maintainer2"] pypi_package_json.pypi_registry.get_maintainer_join_date.side_effect = [datetime(2018, 1, 1), datetime(2019, 1, 1)] pypi_package_json.get_latest_release_upload_time.return_value = "2022-06-20T12:00:00" pypi_package_json.component_name = "mock1" @@ -33,7 +33,7 @@ def test_analyze_process(pypi_package_json: MagicMock) -> None: analyzer = CloserReleaseJoinDateAnalyzer() # Set up mock return values. - pypi_package_json.pypi_registry.get_maintainers_of_package.return_value = ["maintainer1"] + pypi_package_json.get_maintainers_of_package.return_value = ["maintainer1"] pypi_package_json.pypi_registry.get_maintainer_join_date.side_effect = [datetime(2022, 6, 18)] pypi_package_json.get_latest_release_upload_time.return_value = "2022-06-20T12:00:00" pypi_package_json.component_name = "mock1" @@ -52,7 +52,7 @@ def test_analyze_skip(pypi_package_json: MagicMock) -> None: analyzer = CloserReleaseJoinDateAnalyzer() # Set up mock return values. - pypi_package_json.pypi_registry.get_maintainers_of_package.return_value = None + pypi_package_json.get_maintainers_of_package.return_value = None pypi_package_json.get_latest_release_upload_time.return_value = "2022-06-20T12:00:00" pypi_package_json.component_name = "mock1" diff --git a/tests/repo_finder/test_report_schema.py b/tests/repo_finder/test_report_schema.py index 18bfbfa5d..f3fbbbde3 100644 --- a/tests/repo_finder/test_report_schema.py +++ b/tests/repo_finder/test_report_schema.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2026, 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 tests the report schema of the repo finder.""" @@ -17,7 +17,7 @@ def json_schema_() -> Any: """Load and return the JSON schema.""" with open( - os.path.join(MACARON_PATH, "resources", "schemastore", "find_source_report_schema.json"), encoding="utf-8" + os.path.join(MACARON_PATH, "resources", "schemas", "find_source_report_schema.json"), encoding="utf-8" ) as file: return json.load(file)