Skip to content

Commit 21f7bc2

Browse files
authored
feat: add --existing-policy flag in verify-policy command for predefined policies (#1189)
This Pull Request introduces a new --existing-policy flag to the verify-policy command, allowing users to run example policies by name without specifying a file path. It also adds support for policy templates. Signed-off-by: Demolus13 <[email protected]>
1 parent b7caca8 commit 21f7bc2

14 files changed

+332
-4
lines changed

docs/source/pages/tutorials/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ For the full list of supported technologies, such as CI services, registries, an
2323
detect_vulnerable_github_actions
2424
provenance
2525
detect_malicious_java_dep
26+
verify_with_existing_policy
2627
generate_verification_summary_attestation
2728
use_verification_summary_attestation
2829
exclude_include_checks
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
===================================================================
2+
How to use the policy engine to verify with our predefined policies
3+
===================================================================
4+
5+
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.
6+
7+
--------
8+
Use case
9+
--------
10+
11+
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.
12+
13+
-------
14+
Example
15+
-------
16+
17+
Run the ``malware-detection`` policy against a package URL:
18+
19+
.. code-block:: shell
20+
21+
./run_macaron.sh analyze -purl pkg:pypi/[email protected]
22+
23+
.. 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>`.
24+
25+
.. code-block:: shell
26+
27+
./run_macaron.sh verify-policy \
28+
--database output/macaron.db \
29+
--existing-policy malware-detection \
30+
--package-url "pkg:pypi/[email protected]"
31+
32+
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):
33+
34+
.. code-block:: shell
35+
36+
Components Satisfy Policy
37+
1 pkg:pypi/[email protected] check-component
38+
39+
Components Violate Policy None
40+
41+
Passed Policies check-component
42+
Failed Policies None
43+
Policy Report output/policy_report.json
44+
Verification Summary Attestation output/vsa.intoto.jsonl
45+
Decode and Inspect the Content cat output/vsa.intoto.jsonl | jq -r '.payload' | base64 -d | jq
46+
47+
Run the ``malware-detection`` policy using wildcard:
48+
49+
.. code-block:: shell
50+
51+
./run_macaron.sh analyze -purl pkg:pypi/[email protected]
52+
./run_macaron.sh analyze -purl pkg:pypi/[email protected]
53+
54+
.. 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>`.
55+
56+
.. code-block:: shell
57+
58+
./run_macaron.sh verify-policy \
59+
--database output/macaron.db \
60+
--existing-policy malware-detection \
61+
--package-url "pkg:pypi/django@.*"
62+
63+
It uses the wildcard '*' to checks for components satisfying the expression "pkg:pypi/django@.*".
64+
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):
65+
66+
.. code-block:: shell
67+
68+
Components Satisfy Policy
69+
1 pkg:pypi/[email protected] check-component
70+
2 pkg:pypi/[email protected] check-component
71+
72+
Components Violate Policy None
73+
74+
Passed Policies check-component
75+
Failed Policies None
76+
Policy Report output/policy_report.json
77+
Verification Summary Attestation output/vsa.intoto.jsonl
78+
Decode and Inspect the Content cat output/vsa.intoto.jsonl | jq -r '.payload' | base64 -d | jq
79+
80+
-----------------
81+
Related tutorials
82+
-----------------
83+
84+
- :doc:`detect_malicious_package` — shows what the malware-detection policy does in this tutorial.
85+
- :doc:`use_verification_summary_attestation` — how to consume an attestation
86+
produced by Macaron.

src/macaron/__main__.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,22 +196,84 @@ def verify_policy(verify_policy_args: argparse.Namespace) -> int:
196196
int
197197
Returns os.EX_OK if successful or the corresponding error code on failure.
198198
"""
199-
if not os.path.isfile(verify_policy_args.database):
199+
if not verify_policy_args.list_policies and not os.path.isfile(verify_policy_args.database):
200200
logger.critical("The database file does not exist.")
201201
return os.EX_OSFILE
202202

203203
if verify_policy_args.show_prelude:
204204
show_prelude(verify_policy_args.database)
205205
return os.EX_OK
206206

207+
policy_content = None
208+
if verify_policy_args.list_policies:
209+
policy_dir = os.path.join(macaron.MACARON_PATH, "resources", "policies", "datalog")
210+
policy_suffix = ".dl"
211+
template_suffix = f"{policy_suffix}.template"
212+
description_suffix = ".description"
213+
214+
policies_with_desc: dict[str, str] = {}
215+
try:
216+
for policy_file in os.listdir(policy_dir):
217+
if not policy_file.endswith(template_suffix):
218+
continue
219+
policy = os.path.splitext(policy_file)[0].replace(policy_suffix, "")
220+
description_path = os.path.join(policy_dir, f"{policy}{description_suffix}")
221+
try:
222+
with open(description_path, encoding="utf-8") as f:
223+
desc = f.read().strip()
224+
if not desc:
225+
desc = "No description available."
226+
except OSError:
227+
desc = "Could not read policy description."
228+
policies_with_desc[policy] = desc
229+
except FileNotFoundError:
230+
logger.error("Policy directory %s not found.", policy_dir)
231+
return os.EX_OSFILE
232+
233+
policies_with_desc = dict(sorted(policies_with_desc.items()))
234+
rich_handler = access_handler.get_handler()
235+
rich_handler.set_available_policies(policies_with_desc)
236+
237+
logger.info(
238+
"Available policies are:\n%s", "\n".join(f"{name}\n{desc}\n" for name, desc in policies_with_desc.items())
239+
)
240+
return os.EX_OK
241+
207242
if verify_policy_args.file:
208243
if not os.path.isfile(verify_policy_args.file):
209244
logger.critical('The policy file "%s" does not exist.', verify_policy_args.file)
210245
return os.EX_OSFILE
211246

212247
with open(verify_policy_args.file, encoding="utf-8") as file:
213248
policy_content = file.read()
214-
249+
elif verify_policy_args.existing_policy:
250+
policy_dir = os.path.join(macaron.MACARON_PATH, "resources", "policies", "datalog")
251+
policy_suffix = ".dl"
252+
template_suffix = f"{policy_suffix}.template"
253+
available_policies = [
254+
os.path.splitext(policy)[0].replace(policy_suffix, "")
255+
for policy in os.listdir(policy_dir)
256+
if policy.endswith(template_suffix)
257+
]
258+
if verify_policy_args.existing_policy not in available_policies:
259+
logger.error(
260+
"The policy %s is not available. Available policies are: %s",
261+
verify_policy_args.existing_policy,
262+
available_policies,
263+
)
264+
return os.EX_USAGE
265+
policy_path = os.path.join(policy_dir, f"{verify_policy_args.existing_policy}{template_suffix}")
266+
with open(policy_path, encoding="utf-8") as file:
267+
policy_content = file.read()
268+
try:
269+
validation_package_url = verify_policy_args.package_url.replace("*", "")
270+
PackageURL.from_string(validation_package_url)
271+
policy_content = policy_content.replace("<PACKAGE_PURL>", verify_policy_args.package_url)
272+
except ValueError as err:
273+
logger.error("The package url %s is not valid. Error: %s", verify_policy_args.package_url, err)
274+
return os.EX_USAGE
275+
276+
if policy_content:
215277
result = run_policy_engine(verify_policy_args.database, policy_content)
216278
vsa = generate_vsa(policy_content=policy_content, policy_result=result)
217279
# Retrieve the console handler previously configured via the access_handler.
@@ -316,6 +378,9 @@ def perform_action(action_args: argparse.Namespace) -> None:
316378
case "verify-policy":
317379
if not action_args.disable_rich_output:
318380
rich_handler.start("verify-policy")
381+
if not action_args.list_policies and not action_args.database:
382+
logger.error("macaron verify-policy: error: the following arguments are required: -d/--database")
383+
sys.exit(os.EX_USAGE)
319384
sys.exit(verify_policy(action_args))
320385

321386
case "analyze":
@@ -572,8 +637,11 @@ def main(argv: list[str] | None = None) -> None:
572637
vp_parser = sub_parser.add_parser(name="verify-policy")
573638
vp_group = vp_parser.add_mutually_exclusive_group(required=True)
574639

575-
vp_parser.add_argument("-d", "--database", required=True, type=str, help="Path to the database.")
640+
vp_parser.add_argument("-d", "--database", type=str, help="Path to the database.")
641+
vp_parser.add_argument("-purl", "--package-url", help="PackageURL for policy template.")
576642
vp_group.add_argument("-f", "--file", type=str, help="Path to the Datalog policy.")
643+
vp_group.add_argument("-e", "--existing-policy", help="Name of the existing policy to run.")
644+
vp_group.add_argument("-l", "--list-policies", action="store_true", help="List the existing policy to run.")
577645
vp_group.add_argument("-s", "--show-prelude", action="store_true", help="Show policy prelude.")
578646

579647
# Find the repo and commit of a passed PURL, or the commit of a passed PURL and repo.

src/macaron/console.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ def _make_reports_table(reports: dict) -> Table:
7676
table.add_row(report_type, report_path, style="blue")
7777
return table
7878

79+
@staticmethod
80+
def _make_policies_table(policies: dict[str, str]) -> Table:
81+
"""Build a two-column table of policy name and a short description."""
82+
table = Table(box=None)
83+
table.add_column("[blue]Policy[/]", justify="left", style="blue bold")
84+
table.add_column("Description", justify="left")
85+
total_policies = len(policies)
86+
for i, (name, desc) in enumerate(policies.items()):
87+
table.add_row(name, desc)
88+
if i < total_policies - 1:
89+
table.add_row()
90+
return table
91+
7992

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

502+
def set_available_policies(self, policies: dict[str, str]) -> None:
503+
"""
504+
Store available policies and build the policies table.
505+
506+
Parameters
507+
----------
508+
policies : dict[str, str]
509+
Mapping of policy name to short description.
510+
"""
511+
self.available_policies = policies
512+
self.policies_table = self._make_policies_table(self.available_policies)
513+
487514
def update_vsa(self, vsa_path: str) -> None:
488515
"""
489516
Update the verification summary attestation path.
@@ -705,7 +732,9 @@ def make_layout(self) -> Group:
705732
self.report_table,
706733
]
707734
elif self.command == "verify-policy":
708-
if self.policy_summary_table.row_count > 0:
735+
if self.policies_table.row_count > 0:
736+
layout = layout + [self.policies_table]
737+
elif self.policy_summary_table.row_count > 0:
709738
if self.components_satisfy_table.row_count > 0:
710739
layout = layout + [
711740
"[bold green] Components Satisfy Policy[/]",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Policies
2+
=======
3+
4+
This directory contains policy resources used by Macaron. Policies in this folder are packaged as templates that the verify-policy command can use.
5+
6+
Common files and conventions
7+
---------------------------
8+
- `*.dl.template` - datalog policy templates.
9+
- `*.description` - short descriptions that explain the policy's intent.
10+
- `*.cue.template` - CUE-based expectation templates used by the GDK.
11+
12+
Example policies are exposed to the user via Macaron commands `verify-policy --existing-policy <policy-name>`.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Datalog policy templates
2+
=========================
3+
4+
- This folder contains Datalog-based policy templates and accompanying `.description` files used by Macaron's policy engine.
5+
6+
- These `.dl.template` templates are intended as examples and starting points. They can be used by name using `--existing-policy` flag.
7+
8+
- `*.description` - descriptions for each template. These are intended to be shown in UIs or documentation to help users choose an appropriate example policy.
9+
10+
Extending or adding templates
11+
-----------------------------
12+
- Add a new `.dl.template` file and a matching `.description` file.
13+
- Update documentation or the tutorials page if you add new example policies that should be exposed to users.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include "prelude.dl"
2+
3+
Policy("github_actions_vulns", component_id, "GitHub Actions Vulnerability Detection") :-
4+
check_passed(component_id, "mcn_githubactions_vulnerabilities_1").
5+
6+
apply_policy_to("github_actions_vulns", component_id) :-
7+
is_component(component_id, purl),
8+
match("<PACKAGE_PURL>", purl).
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#include "prelude.dl"
2+
3+
Policy("check-dependencies", component_id, "Check the dependencies of component.") :-
4+
transitive_dependency(component_id, dependency),
5+
check_passed(component_id, "mcn_detect_malicious_metadata_1"),
6+
check_passed(dependency, "mcn_detect_malicious_metadata_1").
7+
8+
apply_policy_to("check-dependencies", component_id) :-
9+
is_component(component_id, purl),
10+
match("<PACKAGE_PURL>", purl).

0 commit comments

Comments
 (0)