Policy
The lunar_policy
Python package provides utilities for working with Lunar policies, allowing you to load, query, and make assertions about component metadata, such as the component JSON.
For the reference documentation check out:
Installation
The package is available through pip:
pip install lunar-policy
Policy environment
Earthly Lunar executes policies in an environment set up with the following variables:
LUNAR_HUB_HOST
: The host of the Lunar Hub.LUNAR_HUB_INSECURE
: Whether to skip SSL verification of the Lunar Hub.LUNAR_POLICY_NAME
: The name of the policy being executed.LUNAR_INITIATIVE_NAME
: The name of the initiative the policy belongs to.LUNAR_POLICY_OWNER
: The owner of the policy.LUNAR_COMPONENT_ID
: The ID of the component being checked ingithub.com/.../...
format.LUNAR_COMPONENT_DOMAIN
: The domain of the component.LUNAR_COMPONENT_OWNER
: The owner of the component.LUNAR_COMPONENT_PR
: The PR number of the component, if applicable.LUNAR_COMPONENT_GIT_SHA
: The Git SHA of the component that the policy is being executed for.LUNAR_COMPONENT_TAGS
: The tags of the component.LUNAR_COMPONENT_META
: The metadata of the component as a JSON object.LUNAR_BUNDLE_PATH
: Used internally by Lunar to pass in the component JSON and the deltas to the policy.Any secrets set in the Lunar Hub for the policy, via
LUNAR_POLICY_SECRETS=<name>=<value>;...
.
Core Components
The Policy SDK provides several key classes to help you write policies:
Check
The Check
class provides a fluent interface for making assertions about policy data. It tracks accessed data within the component JSON for traceability purposes.
from lunar_policy import Check, Path
# Create a check with a name and optional description
with Check("my-check", "Validates important properties") as check:
# Get data using JSONPath
value = check.get(".path.to.data")
# Make assertions
check.assert_equals(Path(".api.endpoints[0].method"), "GET")
check.assert_greater_or_equal(value, 50)
check.assert_contains(Path(".api.endpoints[0].path"), "/")
👉 For detailed reference, see the Check reference documentation.
Path
The Path
class is used to pass JSONPath expressions to assertion methods of Check.
👉 See the Path reference documentation.
NoDataError
The NoDataError
exception is used to indicate that required data for a policy check is missing.
👉 For detailed reference, see the NoDataError reference documentation.
PolicyContext
The PolicyContext
class is used to initialize a Check
instance with component metadata from different sources. This can be useful for unit testing policies.
👉 For detailed reference, see the PolicyContext reference documentation.
Automatic Data Loading
When a policy is executed through Lunar Hub, Lunar passes in the relevant context via LUNAR_BUNDLE_PATH
. This context includes the component JSON for the component that is being checked. When you create a Check
instance, the library automatically loads this data under the hood when policy_context
is not provided.
Executing Policies Locally
To execute a policy locally for testing purposes, you can use the lunar
CLI.
lunar policy dev --component-json path/to/component.json ./path/to/policy.py
If you would like to use the real component JSON of one of your components, you can do so via the command:
lunar policy dev --component github.com/my-org/my-repo ./path/to/policy.py
Handling Missing Data
Lunar makes a clear distinction between missing component JSON data and failing component JSON data. This is needed under the hood to be able to provide partial policy results while collectors are still running. Policies that have enough data will provide accurate results, while policies that don't have enough data will report a "no-data" status.
When some or all of the component JSON data required for assertions is not present, policies use the NoDataError
exception mechanism to report this status. The Check
API handles this scenario automatically by catching this exception in the with
context manager, setting the check status to "no-data".
from lunar_policy import Check, NoDataError, Path
# NoDataError can be explicitly raised
with Check("my-check") as check:
if some_condition:
raise NoDataError()
# The `get` method and Path-based assertions automatically raise NoDataError
# when a JSONPath doesn't exist in the component data
with Check("my-check") as check:
# This will raise NoDataError if the path doesn't exist
value = check.get(".path.to.data")
# This will also raise NoDataError if the path doesn't exist
check.assert_true(Path(".api.requires_auth"))
When writing policies, you should:
Be aware that your policy might run against components where the expected data is not present
Know that the
Check
context manager will catchNoDataError
exceptions and set the status to "no-data"Multiple checks in the same code block will continue to execute even if previous checks raise
NoDataError
when wrapping each check in awith
statement.
This approach ensures that policies don't incorrectly fail when run against components that legitimately don't have specific data (for example, because a collector has not had the chance to run yet).
Best Practices for Handling Missing Data
Below are examples demonstrating different approaches to handling missing data in policies:
Bad Approach: Assuming Data Exists
This approach incorrectly assumes all fields exist after retrieving an object, which can lead to errors when data is missing:
with Check("api-security-check") as check:
# Bad: Gets the entire API object once and assumes all fields exist
api = check.get(".api")
# These will cause errors if api is None or missing expected fields
check.assert_true(api["requires_auth"], "API should require authentication")
rate_limit = api["rate_limit"]
check.assert_equals(rate_limit, 100, f"API rate limit should be 100, but found {rate_limit}")
check.assert_contains(api["security_headers"], "Content-Security-Policy")
Better Approach: Using NoData Explicitly
This approach explicitly raises NoData
when fields don't exist:
with Check("api-security-check") as check:
# Gets the API object
api = check.get(".api")
# You can explicitly raise NoDataError if needed for custom conditions
if api is None or "requires_auth" not in api:
raise NoDataError()
# Normal assertions
check.assert_true(api["requires_auth"], "API should require authentication")
Best Approach: Using Path for Automatic NoDataError Handling
This approach uses targeted field access and relies on the Check API to handle missing data automatically:
with Check("api-security-check") as check:
# These assertions automatically raise NoDataError if paths don't exist:
check.assert_true(Path(".api.requires_auth"), "API should require authentication")
# get also raises NoDataError if the path doesn't exist
rate_limit = check.get(".api.rate_limit")
check.assert_equals(rate_limit, 100, f"API rate limit should be 100, but found {rate_limit}")
check.assert_contains(Path(".api.security_headers"), "Content-Security-Policy")
Writing Unit Tests for Policies
Let's assume that we have the following policy:
from lunar_policy import Check, PolicyContext, Path
def verify_readme(policy_context=None):
with Check("readme-exists", policy_context=policy_context) as check:
check.assert_false(Path(".readme.missing"), "README.md should exist")
with Check("readme-long-enough", policy_context=policy_context) as check:
lines = check.get('.readme.lines')
check.assert_greater_or_equal(
lines, 50,
f'README.md should have at least 50 lines. Current count: {lines}'
)
if __name__ == "__main__":
verify_readme()
Here's an example showing how to write unit tests:
import unittest
from lunar_policy import Check, Path, PolicyContext, CheckStatus
class TestReadmePolicy(unittest.TestCase):
def test_not_long_enough(self):
component_json = {
"readme": {
"lines": 49,
"missing": False
}
}
pc = PolicyContext.from_component_json(component_json)
verify_readme(pc)
# Verify the check failed because the README isn't long enough
check = pc.get_last_check()
self.assertEqual(check.status, CheckStatus.FAIL)
self.assertEqual(check.failure_reason, "README.md should have at least 50 lines. Current count: 49")
if __name__ == "__main__":
unittest.main()
You can run the test with:
python -m unittest test_readme_policy.py
Last updated