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
1 change: 1 addition & 0 deletions docs/source/pages/tutorials/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ For the full list of supported technologies, such as CI services, registries, an
detect_vulnerable_github_actions
provenance
detect_malicious_java_dep
verify_with_existing_policy
generate_verification_summary_attestation
use_verification_summary_attestation
exclude_include_checks
Expand Down
86 changes: 86 additions & 0 deletions docs/source/pages/tutorials/verify_with_existing_policy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
===================================================================
How to use the policy engine to verify with our predefined policies
===================================================================

This tutorial shows how to use the ``--existing-policy`` flag with the ``verify-policy`` subcommand to run one of the predefined policies that ship with Macaron.

--------
Use case
--------

Use ``--existing-policy`` when you want to run one of the built-in policies by name instead of providing a local policy file with ``--file``. Pre-defined policies are useful for quick checks or automated examples/tests.

-------
Example
-------

Run the ``malware-detection`` policy against a package URL:

.. code-block:: shell

./run_macaron.sh analyze -purl pkg:pypi/[email protected]

.. note:: By default, Macaron clones the repositories and creates output files under the ``output`` directory. To understand the structure of this directory please see :ref:`Output Files Guide <output_files_guide>`.

.. code-block:: shell

./run_macaron.sh verify-policy \
--database output/macaron.db \
--existing-policy malware-detection \
--package-url "pkg:pypi/[email protected]"

The result of this command should show that the policy succeeds with a zero exit code (if a policy fails to pass, Macaron returns a none-zero error code):

.. code-block:: shell

Components Satisfy Policy
1 pkg:pypi/[email protected] check-component

Components Violate Policy None

Passed Policies check-component
Failed Policies None
Policy Report output/policy_report.json
Verification Summary Attestation output/vsa.intoto.jsonl
Decode and Inspect the Content cat output/vsa.intoto.jsonl | jq -r '.payload' | base64 -d | jq

Run the ``malware-detection`` policy using wildcard:

.. code-block:: shell

./run_macaron.sh analyze -purl pkg:pypi/[email protected]
./run_macaron.sh analyze -purl pkg:pypi/[email protected]

.. note:: By default, Macaron clones the repositories and creates output files under the ``output`` directory. To understand the structure of this directory please see :ref:`Output Files Guide <output_files_guide>`.

.. code-block:: shell

./run_macaron.sh verify-policy \
--database output/macaron.db \
--existing-policy malware-detection \
--package-url "pkg:pypi/django@.*"

It uses the wildcard '*' to checks for components satisfying the expression "pkg:pypi/django@.*".
The result of this command should show that the policy succeeds with a zero exit code (if a policy fails to pass, Macaron returns a none-zero error code):

.. code-block:: shell

Components Satisfy Policy
1 pkg:pypi/[email protected] check-component
2 pkg:pypi/[email protected] check-component

Components Violate Policy None

Passed Policies check-component
Failed Policies None
Policy Report output/policy_report.json
Verification Summary Attestation output/vsa.intoto.jsonl
Decode and Inspect the Content cat output/vsa.intoto.jsonl | jq -r '.payload' | base64 -d | jq

-----------------
Related tutorials
-----------------

- :doc:`detect_malicious_package` — shows what the malware-detection policy does in this tutorial.
- :doc:`use_verification_summary_attestation` — how to consume an attestation
produced by Macaron.
74 changes: 71 additions & 3 deletions src/macaron/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,22 +196,84 @@ def verify_policy(verify_policy_args: argparse.Namespace) -> int:
int
Returns os.EX_OK if successful or the corresponding error code on failure.
"""
if not os.path.isfile(verify_policy_args.database):
if not verify_policy_args.list_policies and not os.path.isfile(verify_policy_args.database):
logger.critical("The database file does not exist.")
return os.EX_OSFILE

if verify_policy_args.show_prelude:
show_prelude(verify_policy_args.database)
return os.EX_OK

policy_content = None
if verify_policy_args.list_policies:
policy_dir = os.path.join(macaron.MACARON_PATH, "resources", "policies", "datalog")
policy_suffix = ".dl"
template_suffix = f"{policy_suffix}.template"
description_suffix = ".description"

policies_with_desc: dict[str, str] = {}
try:
for policy_file in os.listdir(policy_dir):
if not policy_file.endswith(template_suffix):
continue
policy = os.path.splitext(policy_file)[0].replace(policy_suffix, "")
description_path = os.path.join(policy_dir, f"{policy}{description_suffix}")
try:
with open(description_path, encoding="utf-8") as f:
desc = f.read().strip()
if not desc:
desc = "No description available."
except OSError:
desc = "Could not read policy description."
policies_with_desc[policy] = desc
except FileNotFoundError:
logger.error("Policy directory %s not found.", policy_dir)
return os.EX_OSFILE

policies_with_desc = dict(sorted(policies_with_desc.items()))
rich_handler = access_handler.get_handler()
rich_handler.set_available_policies(policies_with_desc)

logger.info(
"Available policies are:\n%s", "\n".join(f"{name}\n{desc}\n" for name, desc in policies_with_desc.items())
)
return os.EX_OK

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)
return os.EX_OSFILE

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:
validation_package_url = verify_policy_args.package_url.replace("*", "")
PackageURL.from_string(validation_package_url)
policy_content = policy_content.replace("<PACKAGE_PURL>", 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.
Expand Down Expand Up @@ -316,6 +378,9 @@ def perform_action(action_args: argparse.Namespace) -> None:
case "verify-policy":
if not action_args.disable_rich_output:
rich_handler.start("verify-policy")
if not action_args.list_policies and not action_args.database:
logger.error("macaron verify-policy: error: the following arguments are required: -d/--database")
sys.exit(os.EX_USAGE)
sys.exit(verify_policy(action_args))

case "analyze":
Expand Down Expand Up @@ -572,8 +637,11 @@ def main(argv: list[str] | None = None) -> None:
vp_parser = sub_parser.add_parser(name="verify-policy")
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("-d", "--database", 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("-l", "--list-policies", action="store_true", help="List 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.
Expand Down
31 changes: 30 additions & 1 deletion src/macaron/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ def _make_reports_table(reports: dict) -> Table:
table.add_row(report_type, report_path, style="blue")
return table

@staticmethod
def _make_policies_table(policies: dict[str, str]) -> Table:
"""Build a two-column table of policy name and a short description."""
table = Table(box=None)
table.add_column("[blue]Policy[/]", justify="left", style="blue bold")
table.add_column("Description", justify="left")
total_policies = len(policies)
for i, (name, desc) in enumerate(policies.items()):
table.add_row(name, desc)
if i < total_policies - 1:
table.add_row()
return table


class Dependency(TableBuilder):
"""A class to manage the display of dependency analysis in the console."""
Expand Down Expand Up @@ -289,6 +302,8 @@ def __init__(self, *args: Any, verbose: bool = False, **kwargs: Any) -> None:
self.if_dependency: bool = False
self.dependency_analysis_map: dict[str, int] = {}
self.dependency_analysis_list: list[Dependency] = []
self.available_policies: dict[str, str] = {}
self.policies_table = Table(box=None)
self.components_violates_table = Table(box=None)
self.components_satisfy_table = Table(box=None)
self.policy_summary_table = Table(show_header=False, box=None)
Expand Down Expand Up @@ -484,6 +499,18 @@ def update_policy_report(self, report_path: str) -> None:
self.policy_summary["Policy Report"] = report_path
self.generate_policy_summary_table()

def set_available_policies(self, policies: dict[str, str]) -> None:
"""
Store available policies and build the policies table.

Parameters
----------
policies : dict[str, str]
Mapping of policy name to short description.
"""
self.available_policies = policies
self.policies_table = self._make_policies_table(self.available_policies)

def update_vsa(self, vsa_path: str) -> None:
"""
Update the verification summary attestation path.
Expand Down Expand Up @@ -705,7 +732,9 @@ def make_layout(self) -> Group:
self.report_table,
]
elif self.command == "verify-policy":
if self.policy_summary_table.row_count > 0:
if self.policies_table.row_count > 0:
layout = layout + [self.policies_table]
elif self.policy_summary_table.row_count > 0:
if self.components_satisfy_table.row_count > 0:
layout = layout + [
"[bold green] Components Satisfy Policy[/]",
Expand Down
12 changes: 12 additions & 0 deletions src/macaron/resources/policies/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Policies
=======

This directory contains policy resources used by Macaron. Policies in this folder are packaged as templates that the verify-policy command can use.

Common files and conventions
---------------------------
- `*.dl.template` - datalog policy templates.
- `*.description` - short descriptions that explain the policy's intent.
- `*.cue.template` - CUE-based expectation templates used by the GDK.

Example policies are exposed to the user via Macaron commands `verify-policy --existing-policy <policy-name>`.
13 changes: 13 additions & 0 deletions src/macaron/resources/policies/datalog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Datalog policy templates
=========================

- This folder contains Datalog-based policy templates and accompanying `.description` files used by Macaron's policy engine.

- These `.dl.template` templates are intended as examples and starting points. They can be used by name using `--existing-policy` flag.

- `*.description` - descriptions for each template. These are intended to be shown in UIs or documentation to help users choose an appropriate example policy.

Extending or adding templates
-----------------------------
- Add a new `.dl.template` file and a matching `.description` file.
- Update documentation or the tutorials page if you add new example policies that should be exposed to users.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Detects whether a component was built using GitHub Actions that are known to be vulnerable or otherwise unsafe. The policy evaluates a check named `mcn_githubactions_vulnerabilities_1` and reports a passed/failed result for the component when applied.
Original file line number Diff line number Diff line change
@@ -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("<PACKAGE_PURL>", purl).
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Checks the component and its transitive dependencies for indicators of malicious or suspicious content. The policy ensures the component and each dependency pass the `mcn_detect_malicious_metadata_1` check.
Original file line number Diff line number Diff line change
@@ -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("<PACKAGE_PURL>", purl).
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Checks a component for indicators of malicious or suspicious content. The policy evaluates a check named mcn_detect_malicious_metadata_1 and reports a passed/failed result for the component when applied.
Original file line number Diff line number Diff line change
@@ -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("<PACKAGE_PURL>", purl).
24 changes: 24 additions & 0 deletions tests/policy_engine/test_datalog_description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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/.

"""Tests that every Datalog template has a matching .description file."""

from pathlib import Path

import macaron


def test_datalog_templates_have_descriptions() -> None:
"""Verify each ``*.dl.template`` has a corresponding ``*.description``."""
datalog_dir = Path(macaron.__file__).resolve().parent.joinpath("resources", "policies", "datalog")
templates = sorted(datalog_dir.glob("*.dl.template"))

missing = []
for tmpl in templates:
expected_desc = datalog_dir.joinpath(tmpl.name.replace(".dl.template", ".description"))
if not expected_desc.exists():
missing.append((tmpl.name, expected_desc))

if templates and missing:
missing_list = ", ".join(f"{t} -> {d}" for t, d in missing)
raise AssertionError("Missing .description files for the following templates: " + missing_list)
Loading
Loading