diff --git a/src/macaron/__main__.py b/src/macaron/__main__.py index c887a721e..a0901b07a 100644 --- a/src/macaron/__main__.py +++ b/src/macaron/__main__.py @@ -204,6 +204,7 @@ def verify_policy(verify_policy_args: argparse.Namespace) -> int: show_prelude(verify_policy_args.database) return os.EX_OK + policy_content = None if verify_policy_args.file: if not os.path.isfile(verify_policy_args.file): logger.critical('The policy file "%s" does not exist.', verify_policy_args.file) @@ -211,7 +212,33 @@ def verify_policy(verify_policy_args: argparse.Namespace) -> int: with open(verify_policy_args.file, encoding="utf-8") as file: policy_content = file.read() + elif verify_policy_args.existing_policy: + policy_dir = os.path.join(macaron.MACARON_PATH, "resources", "policies", "datalog") + policy_suffix = ".dl" + template_suffix = f"{policy_suffix}.template" + available_policies = [ + os.path.splitext(policy)[0].replace(policy_suffix, "") + for policy in os.listdir(policy_dir) + if policy.endswith(template_suffix) + ] + if verify_policy_args.existing_policy not in available_policies: + logger.error( + "The policy %s is not available. Available policies are: %s", + verify_policy_args.existing_policy, + available_policies, + ) + return os.EX_USAGE + policy_path = os.path.join(policy_dir, f"{verify_policy_args.existing_policy}{template_suffix}") + with open(policy_path, encoding="utf-8") as file: + policy_content = file.read() + try: + PackageURL.from_string(verify_policy_args.package_url) + policy_content = policy_content.replace("", verify_policy_args.package_url) + except ValueError as err: + logger.error("The package url %s is not valid. Error: %s", verify_policy_args.package_url, err) + return os.EX_USAGE + if policy_content: result = run_policy_engine(verify_policy_args.database, policy_content) vsa = generate_vsa(policy_content=policy_content, policy_result=result) # Retrieve the console handler previously configured via the access_handler. @@ -573,7 +600,9 @@ def main(argv: list[str] | None = None) -> None: vp_group = vp_parser.add_mutually_exclusive_group(required=True) vp_parser.add_argument("-d", "--database", required=True, type=str, help="Path to the database.") + vp_parser.add_argument("-purl", "--package-url", help="PackageURL for policy template.") vp_group.add_argument("-f", "--file", type=str, help="Path to the Datalog policy.") + vp_group.add_argument("-e", "--existing-policy", help="Name of the existing policy to run.") vp_group.add_argument("-s", "--show-prelude", action="store_true", help="Show policy prelude.") # Find the repo and commit of a passed PURL, or the commit of a passed PURL and repo. diff --git a/src/macaron/resources/policies/datalog/check-github-actions.dl.template b/src/macaron/resources/policies/datalog/check-github-actions.dl.template new file mode 100644 index 000000000..bfd0b04d3 --- /dev/null +++ b/src/macaron/resources/policies/datalog/check-github-actions.dl.template @@ -0,0 +1,8 @@ +#include "prelude.dl" + +Policy("github_actions_vulns", component_id, "GitHub Actions Vulnerability Detection") :- + check_passed(component_id, "mcn_githubactions_vulnerabilities_1"). + +apply_policy_to("github_actions_vulns", component_id) :- + is_component(component_id, purl), + match("@.*", purl). diff --git a/src/macaron/resources/policies/datalog/malware-detection-dependencies.dl.template b/src/macaron/resources/policies/datalog/malware-detection-dependencies.dl.template new file mode 100644 index 000000000..55c2adca1 --- /dev/null +++ b/src/macaron/resources/policies/datalog/malware-detection-dependencies.dl.template @@ -0,0 +1,10 @@ +#include "prelude.dl" + +Policy("check-dependencies", component_id, "Check the dependencies of component.") :- + transitive_dependency(component_id, dependency), + check_passed(component_id, "mcn_detect_malicious_metadata_1"), + check_passed(dependency, "mcn_detect_malicious_metadata_1"). + +apply_policy_to("check-dependencies", component_id) :- + is_component(component_id, purl), + match("@.*", purl). diff --git a/src/macaron/resources/policies/datalog/malware-detection.dl.template b/src/macaron/resources/policies/datalog/malware-detection.dl.template new file mode 100644 index 000000000..38bff2c4b --- /dev/null +++ b/src/macaron/resources/policies/datalog/malware-detection.dl.template @@ -0,0 +1,9 @@ +#include "prelude.dl" + +Policy("check-component", component_id, "Check component artifacts.") :- + check_passed(component_id, "mcn_detect_malicious_metadata_1"). + + +apply_policy_to("check-component", component_id) :- + is_component(component_id, purl), + match("@.*", purl). diff --git a/tests/policy_engine/test_existing_policy.py b/tests/policy_engine/test_existing_policy.py new file mode 100644 index 000000000..359c07f1d --- /dev/null +++ b/tests/policy_engine/test_existing_policy.py @@ -0,0 +1,61 @@ +# Copyright (c) 2025 - 2025, 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 existing-policy flag supported by the policy engine.""" + +import argparse +import os +from pathlib import Path +from unittest.mock import MagicMock, patch + +from macaron.__main__ import verify_policy + + +def test_verify_existing_policy_success(tmp_path: Path) -> None: + """When an existing policy is provided and package-url is valid, verify_policy returns EX_OK.""" + db_file = tmp_path / "macaron.db" + db_file.write_text("") + + # Use a MagicMock for the handler. + mock_handler = MagicMock() + + # Fake run_policy_engine and generate_vsa that returns a fixed result. + fake_run = MagicMock(return_value={"passed_policies": [["check-component"]], "failed_policies": []}) + fake_generate_vsa = MagicMock(return_value=None) + + # Fake PolicyReporter class: when called, returns an instance with generate method. + fake_policy_reporter_cls = MagicMock() + fake_policy_reporter_inst = MagicMock() + fake_policy_reporter_inst.generate.return_value = None + fake_policy_reporter_cls.return_value = fake_policy_reporter_inst + + with ( + patch("macaron.__main__.run_policy_engine", fake_run), + patch("macaron.__main__.generate_vsa", fake_generate_vsa), + patch("macaron.__main__.access_handler.get_handler", return_value=mock_handler), + patch("macaron.__main__.PolicyReporter", fake_policy_reporter_cls), + ): + policy_args = argparse.Namespace( + database=str(db_file), + show_prelude=False, + file=None, + existing_policy="malware-detection", + package_url="pkg:pypi/django", + ) + result = verify_policy(policy_args) + assert result == os.EX_OK + + +def test_verify_existing_policy_not_found(tmp_path: Path) -> None: + """Requesting a non-existent policy returns usage error.""" + db_file = tmp_path / "macaron.db" + db_file.write_text("") + policy_args = argparse.Namespace( + database=str(db_file), + show_prelude=False, + file=None, + existing_policy="no-such-policy", + package_url="pkg:pypi/django", + ) + result = verify_policy(policy_args) + assert result == os.EX_USAGE