Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions action.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
8 changes: 8 additions & 0 deletions docs/source/pages/cli_usage/command_gen_build_spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/oracle/macaron/tree/main/src/macaron/resources/schemas/macaron_buildspec_schema.json>`_ of the repository. Be sure to use the schema that matches your Macaron release by selecting the appropriate GitHub tag.
6 changes: 3 additions & 3 deletions docs/source/pages/macaron_action.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <gen-build-spec-schema>`):

.. code-block:: ini

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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."""
Expand All @@ -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):
Expand All @@ -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)

Expand Down Expand Up @@ -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": (
Expand Down
Original file line number Diff line number Diff line change
@@ -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."""
Expand Down Expand Up @@ -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.
Expand Down
130 changes: 130 additions & 0 deletions src/macaron/resources/schemas/macaron_buildspec_schema.json
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 19 additions & 20 deletions src/macaron/slsa_analyzer/package_registry/pypi_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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: |
Expand Down Expand Up @@ -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
8 changes: 7 additions & 1 deletion tests/integration/cases/pypi_toga/test.yaml
Original file line number Diff line number Diff line change
@@ -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: |
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion tests/integration/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
}


Expand Down
Loading
Loading