From 5b65c26a58461e1d92896eab58e50cc40ac81ae1 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:49:22 +0000 Subject: [PATCH 01/35] refactor(epm): introduce baseline policies. --- README.md | 647 +++--------------- .../huggingface-recipes}/README.md | 0 .../huggingface-recipes}/model_card.rego | 0 .../huggingface-recipes}/risky_files.rego | 0 .../huggingface-recipes}/security_scan.rego | 0 .../trusted_publishers.rego | 0 baseline/cooldown-restore.rego | 27 + baseline/cooldown.rego | 23 + baseline/exact-allowlist-exemption.rego | 29 + baseline/exact-blocklist.rego | 29 + baseline/high-risk-vulnerability.rego | 134 ++++ baseline/license-compliance.rego | 118 ++++ baseline/malware-block.rego | 18 + {recipe-1 => legacy/recipe-1}/policy.rego | 0 {recipe-10 => legacy/recipe-10}/policy.rego | 0 {recipe-11 => legacy/recipe-11}/policy.rego | 0 {recipe-12 => legacy/recipe-12}/policy.rego | 0 {recipe-13 => legacy/recipe-13}/policy.rego | 0 {recipe-14 => legacy/recipe-14}/policy.rego | 0 {recipe-15 => legacy/recipe-15}/policy.rego | 0 {recipe-16 => legacy/recipe-16}/policy.rego | 0 {recipe-17 => legacy/recipe-17}/policy.rego | 0 {recipe-18 => legacy/recipe-18}/policy.rego | 0 {recipe-19 => legacy/recipe-19}/policy.rego | 0 {recipe-2 => legacy/recipe-2}/policy.rego | 0 {recipe-20 => legacy/recipe-20}/policy.rego | 0 {recipe-21 => legacy/recipe-21}/policy.rego | 0 {recipe-3 => legacy/recipe-3}/policy.rego | 0 {recipe-4 => legacy/recipe-4}/policy.rego | 0 {recipe-5 => legacy/recipe-5}/policy.rego | 0 {recipe-6 => legacy/recipe-6}/policy.rego | 0 {recipe-7 => legacy/recipe-7}/policy.rego | 0 {recipe-8 => legacy/recipe-8}/policy.rego | 0 {recipe-9 => legacy/recipe-9}/policy.rego | 0 34 files changed, 489 insertions(+), 536 deletions(-) rename {huggingface-recipes => advanced/huggingface-recipes}/README.md (100%) rename {huggingface-recipes => advanced/huggingface-recipes}/model_card.rego (100%) rename {huggingface-recipes => advanced/huggingface-recipes}/risky_files.rego (100%) rename {huggingface-recipes => advanced/huggingface-recipes}/security_scan.rego (100%) rename {huggingface-recipes => advanced/huggingface-recipes}/trusted_publishers.rego (100%) create mode 100644 baseline/cooldown-restore.rego create mode 100644 baseline/cooldown.rego create mode 100644 baseline/exact-allowlist-exemption.rego create mode 100644 baseline/exact-blocklist.rego create mode 100644 baseline/high-risk-vulnerability.rego create mode 100644 baseline/license-compliance.rego create mode 100644 baseline/malware-block.rego rename {recipe-1 => legacy/recipe-1}/policy.rego (100%) rename {recipe-10 => legacy/recipe-10}/policy.rego (100%) rename {recipe-11 => legacy/recipe-11}/policy.rego (100%) rename {recipe-12 => legacy/recipe-12}/policy.rego (100%) rename {recipe-13 => legacy/recipe-13}/policy.rego (100%) rename {recipe-14 => legacy/recipe-14}/policy.rego (100%) rename {recipe-15 => legacy/recipe-15}/policy.rego (100%) rename {recipe-16 => legacy/recipe-16}/policy.rego (100%) rename {recipe-17 => legacy/recipe-17}/policy.rego (100%) rename {recipe-18 => legacy/recipe-18}/policy.rego (100%) rename {recipe-19 => legacy/recipe-19}/policy.rego (100%) rename {recipe-2 => legacy/recipe-2}/policy.rego (100%) rename {recipe-20 => legacy/recipe-20}/policy.rego (100%) rename {recipe-21 => legacy/recipe-21}/policy.rego (100%) rename {recipe-3 => legacy/recipe-3}/policy.rego (100%) rename {recipe-4 => legacy/recipe-4}/policy.rego (100%) rename {recipe-5 => legacy/recipe-5}/policy.rego (100%) rename {recipe-6 => legacy/recipe-6}/policy.rego (100%) rename {recipe-7 => legacy/recipe-7}/policy.rego (100%) rename {recipe-8 => legacy/recipe-8}/policy.rego (100%) rename {recipe-9 => legacy/recipe-9}/policy.rego (100%) diff --git a/README.md b/README.md index b28f95f..e6a2819 100644 --- a/README.md +++ b/README.md @@ -1,537 +1,112 @@ # Cloudsmith EPM Recipes -A curated collection of Enterprise Policy Management (EPM) recipes for Cloudsmith - combining practical OPA Rego policy samples, action configurations, and testing guides to help you define, simulate, and enforce package lifecycle rules across your repositories. -

-This repository includes: -- Rego policies tailored for Cloudsmith’s EPM engine, using the supported input schema and policy format. -- Policy action configurations (```SetPackageState```, ```AddPackageTags```, etc.) with examples showing how to order and associate them by precedence. -- Simulation and deployment instructions using Cloudsmith’s API, including how to safely test policies before enforcement. -- Helper scripts and workflows to automate policy testing and promotion. - -These recipes are designed to be modular, auditable, and production-ready - with a strong emphasis on policy-as-code best practices. - -*** - -### Table of Rego Samples - -| Name | Description | -| --- | --- | -| [Enforcing Signed Packages](https://github.com/cloudsmith-io/rego-recipes?tab=readme-ov-file#recipe-1---enforcing-signed-packages) | This policy enforces mandatory ```GPG/DSA signature``` checks on packages during their sync/import into Cloudsmith | -| [Restriction Based on Tags](https://github.com/cloudsmith-io/rego-recipes?tab=readme-ov-file#recipe-2---restricting-package-based-on-tags) | This policy checks whether a package includes specific ```deprecated``` tag and marks it as match if present | -| [Copy-Left licensing](https://github.com/cloudsmith-io/rego-recipes?tab=readme-ov-file#recipe-3---copyleft-or-restrictive-oss-licenses) | This policy is designed to detect a broad range of copyleft licenses, including free-text and SPDX variants | -| [Quarantine Debug Builds](https://github.com/cloudsmith-io/rego-recipes?tab=readme-ov-file#recipe-4---restricting-package-based-on-tags) | Identify and quarantine packages that look like debug/test artifacts based on filename or metadata patterns | -| [Limit Tag Sprawl](https://github.com/cloudsmith-io/rego-recipes?tab=readme-ov-file#recipe-5---limit-tag-sprawl) | Flag any packages that have more than a threshold number of tags to avoid ungoverned tagging behaviours | -| [Enforce Consistent Filename](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-6---enforce-consistent-filename-convention) | Validate whether the filename convention matches a semantic or naming pattern via Regular Expressions | -| [Approved Upstreams based on Tags](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-7---approved-upstreams-based-on-tags) | Only packages from explicitly approved upstream sources are permitted, helping to prevent the propagation of unvetted or insecure dependencies. | -| [CVSS Policy with Fix Available](https://github.com/cloudsmith-io/rego-recipes?tab=readme-ov-file#recipe-8---cvss-with-fix-available) | Match packages in a specific repo that have high/critical fixed vulnerabilities, excluding specific known CVEs. | -| [Time-Based CVSS Policy](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-9---time-based-cvss-policy) | Evaluate CVEs older than 30 days. Checks CVSS threshold ≥ 7. Filters for a specific repo. Ignores certain CVE | -| CVSS with EPSS context | Combines High scoring CVSS vulnerability with EPSS scoring context that go above a specific threshold. | -| [Architecture allow list](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-11---architecture-specific-allow-list) | Policy that only allows ```amd64``` architecture packages and blocks others like arm64. | -| [Block package if version over 0.16.0](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-12---block-package-xyz-if-version--0160) | ```semver.compare(pkg.version, "0.16.0") == -1``` should check if the version is less than ```0.16.0``` using semver-aware comparison. | -| [Block specific CVE numbers](https://github.com/cloudsmith-io/rego-recipes?tab=readme-ov-file#recipe-15---block-specifically-based-on-cves) | Blocks a specific package based on known CVE numbers | -| [Enforce Upload Time Window](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-16---suspicious-package-upload-window) | Allow uploads during business hours (9 AM – 5 PM UTC), to catch anomalous behaviour like late-night uploads | -| [Tag-based bypass Exception](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-17---tag-based-exception-policy) | This is a simple tag-based exception. | -| [Exact allowlist with CVSS limit exemption](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-18---exact-allowlist-exception-policy-with-cvss-ceiling) | Use when you want tight control per version, but still prevent exemptions if a CVSS exceeds a ceiling. | -| [Malware advisory](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-19---malware-advisory) | Match for malware advisory. | -| [npm last published date](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-20---npm-last-published-date) | Use when you want to tag or stop devs from using the latest npm package. | -| [Exact blocklist by format/name/version](https://github.com/cloudsmith-io/rego-recipes/tree/main?tab=readme-ov-file#recipe-21---exact-blocklist) | Blocks packages that appear on a known-bad exact list across formats (e.g. npm/python) before your upstream removes them. | -| [Huggingface Recipes](https://github.com/cloudsmith-io/rego-recipes/blob/main/huggingface-recipes/README.md/) | Policies relating to Hugging Face models/datasets. | - -*** - -### Recipe 1 - Enforcing Signed Packages -This policy enforces mandatory GPG/DSA signature checks on packages during their sync/import into Cloudsmith
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-1/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Enforcing Signed Packages", - "description": "This policy enforces mandatory GPG/DSA signature checks on packages during their sync/import into Cloudsmith.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 1 -} -EOF -``` - -Once the policy is created, here is how you can perform a controlled test.
-Create a simple dummy python package locally that we know for certain is unsigned.
-Your policy will trigger, since ```input.v0.package.signed``` will not be present or will default to ```false```. - -``` -mkdir dummy_unsigned -cd dummy_unsigned -echo "from setuptools import setup; setup(name='dummy_unsigned', version='0.0.1')" > setup.py -python3 -m pip install --upgrade build -apt install python3.10-venv -python3 -m build # produces dist/dummy_unsigned-0.0.1.tar.gz -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO dist/dummy_unsigned-0.0.1.tar.gz -k "$CLOUDSMITH_API_KEY" -``` - -*** - -### Recipe 2 - Restricting Package Based on Tags -This policy checks whether a package includes a specific ```deprecated``` tag and marks it as a match if it does.
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-2/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Restricting Package Based on Tags", - "description": "This policy checks whether a package includes a specific DEPRECATED tag.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 2 -} -EOF -``` - -Once ready, download a random package, in this case a python package called ```h11``` and push the package to Cloudsmith with the ```deprecated``` tag to cause the policy violation: -``` -pip download h11==0.14.0 -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO h11-0.14.0-py3-none-any.whl -k "$CLOUDSMITH_API_KEY" --tags deprecated -``` - -*** - -### Recipe 3 - Copyleft or restrictive OSS licenses -This policy is designed to flag packages that use ```copyleft``` or ```restrictive``` open-source licenses, particularly those unsuitable for production use without legal review or approval.
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-3/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Copyleft licensing policy", - "description": "This policy checks packages that are unsuitable for production.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 3 -} -EOF -``` - -Once ready, download the Python ```Gitlab v.3.1.1``` package that we know has a ```LGPLv3 license``` that should trigger the policy: -``` -pip download python-gitlab==3.1.1 -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO python_gitlab-3.1.1-py3-none-any.whl -k "$CLOUDSMITH_API_KEY" --tags lgplv3license -cloudsmith list packages acme-corporation/acme-repo-one -k "$CLOUDSMITH_API_KEY" -q "format:python AND tag:lgplv3license" -``` - -Note: If you have a tagging response action attached to your policy, you could tag the package with ```non-compliant-license``` for further review: - -Screenshot 2025-07-30 at 14 21 19 - -Screenshot 2025-07-30 at 14 28 43 - - -*** - -### Recipe 4 - Quarantine Debug Builds -This policy checks whether a package includes specific ```debug```, ```test```, or ```temp``` descriptors in the filename. -Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-4/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Quarantine Debug Builds", - "description": "Identify and quarantine packages that look like debug/test artifacts based on filename or metadata patterns.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 4 -} -EOF -``` - -Once ready, download ```debugpy``` Python package and push it to Cloudsmith to cause the policy violation: -``` -pip download --no-deps --dest . debugpy -&& cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl -k "$CLOUDSMITH_API_KEY" -``` - -*** - -### Recipe 5 - Limit Tag Sprawl -This policy checks whether a package already includes ```5``` or more assigned tags - conidered as sprawl by some orgs. -Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-5/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Limit Tag Sprawl", - "description": "Flag packages that have more than a threshold number of tags to avoid ungoverned tagging behaviours.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 5 -} -EOF -``` - -Once ready, download ```transformers``` Python and assign it ```5 tags``` during the Cloudsmith push process to cause the policy violation: -``` -pip download transformers --no-deps -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO transformers-4.53.1-py3-none-any.whl -k "$CLOUDSMITH_API_KEY" --tags TAG1,TAG2,TAG3,TAG4,TAG5 -``` - -*** - -### Recipe 6 - Enforce Consistent Filename Convention -Validate filename matches a semantic or naming pattern where ```MAJOR```.```MINOR```, and ```PATCH``` are all numeric. -Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-6/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Enforce Consistent Filename Convention", - "description": "Validate filename matches a semantic or naming pattern.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 6 -} -EOF -``` - -A straightforward way to test this policy is to take a package that already has a valid SemVer-compliant filename and rename it by replacing the version number with a placeholder like ```test```: - -``` -pip download h11==0.14.0 -mv h11-0.14.0-py3-none-any.whl h11-test.whl -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO h11-test.whl -k "$CLOUDSMITH_API_KEY" -``` - -*** - -### Recipe 7 - Approved Upstreams based on Tags -Simply put, this approves packages based on the specified upstream source.
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-7/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Approved Upstreams based on Tags", - "description": "Only packages from explicitly approved upstream sources are permitted, helping to prevent the propagation of unvetted or insecure dependencies.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 7 -} -EOF -``` - -If a package has ```upstream``` and not ```approved``` --> allowed -

-If a package has ```approved``` --> blocked (even if upstream is present) - - -*** - -### Recipe 8 - CVSS with Fix Available -This policy is designed to match packages in a specific repository (```acme-repo-one```) that have ```high``` or ```critical``` with a ```Fixed version available```, excluding specific ```known CVEs```. -Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-8/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "CVSS with Fix Available", - "description": "Matched packages from a specific repository that have high or critical vulnerabilities that can be patched.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 8 -} -EOF -``` - -To demonstrate this policy, you can use the ```requests``` Python package, which has a known vulnerability with a high CVSS score. -

-Vulnerability Details: -
-- Package: h11 -- Affected Version: 0.14.0 -- Fixed In: 0.16.0 -- CVE Identifier: [CVE-2025-43859](https://access.redhat.com/security/cve/cve-2025-43859) -- CVSS Context: This CVE record has been marked for NVD enrichment efforts. -- Description: An HTTP request smuggling vulnerability in python-h11. - -``` -pip download h11==0.14.0 -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO h11-0.14.0-py3-none-any.whl -k "$CLOUDSMITH_API_KEY" -``` - -You'll probably want to enable a [Quarantine](https://help.cloudsmith.io/docs/package-quarantine) action for policies dealing with critical vulnerabilities that can be fixed: - -Screenshot 2025-07-28 at 10 28 21 - - -*** - -### Recipe 9 - Time-based CVSS Policy -This policy is designed to detect and flag packages in a specific repo that contain serious, outdated, but fixed vulnerabilities. -Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-9/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Time-based CVSS policy", - "description": "Only matches if the vulnerability was published more than 30 days ago.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 9 -} -EOF -``` - -This CVE was published on 24 April 2025 - much older than the 30 day threshold specified in the policy. - -``` -pip download h11==0.14.0 -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO h11-0.14.0-py3-none-any.whl -k "$CLOUDSMITH_API_KEY" -``` - -*** - - -### Recipe 11 - Architecture-specific allow list -This policy only allows ```amd64``` architecture packages and blocks others like ```arm64```. -Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-11/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Architecture-specific allow list", - "description": "Policy that only allows amd64 architecture packages and blocks others like arm64", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 11 -} -EOF -``` - -To trigger the above policy (which blocks all architectures except ```amd64```), you'd want to upload or sync a Python package that is built for a ```non-amd64``` architecture - like ```arm64```. -

-However, pip by default fetches packages built for your local system architecture, so you typically won't download architecture-specific wheels unless they're explicitly tagged. -

-Here's a command to download a known Python package with an ARM-specific wheel using pip download: -``` -pip download numpy --platform manylinux2014_aarch64 --only-binary=:all: --python-version 38 --implementation cp --abi cp38 -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -k "$CLOUDSMITH_API_KEY" -``` - -- ```--platform manylinux2014_aarch64```: targets ```arm64``` (```aarch64```) -- ```--only-binary=:all:```: avoid source dist, force binary wheel -- ```--python-version 38``` and ```--abi cp38```: simulate Python 3.8 CPython ABI - -Screenshot 2025-07-28 at 11 26 50 - - -*** - - -### Recipe 12 - Block Package XYZ if version < 0.16.0 -This policy matches any ```h11``` packages with a version older than ```0.16.0```:
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-12/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Forced Upgrade of Package Versions", - "description": "Forces teams to periodically upgrade versions of certain package dependencies, so they don't fall too far behind.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 12 -} -EOF -``` - -*** - -### Recipe 15 - Block specifically based on CVEs -Again, the Python package ```requests``` version ```2.6.0``` has the known vulnerability ```CVE-2018-18074```:
-We have commented out the other CVEs in this policy, but feel free remove those comments and add additional CVEs as a list.
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-15/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) - -cat < payload.json -{ - "name": "Block specific CVE numbers", - "description": "This policy is only blocking CVE-2018-18074 specifically", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 15 -} -EOF -``` - -``` -pip download requests==2.6.0 -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO requests-2.6.0-py2.py3-none-any.whl -k "$CLOUDSMITH_API_KEY" -``` - -Screenshot 2025-07-29 at 13 43 39 - - -*** - - -### Recipe 16 - Suspicious Package Upload Window -Assuming your organisation don't expect packages to be uploaded or modified at specific times, the below policy can detect package uploads at ```9am-11am UTC``` - regardless of the package name.
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-16/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) - -cat < payload.json -{ - "name": "Package Upload Window", - "description": "Flag any package uploaded outside of working hours", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 16 -} -EOF -``` - -``` -pip download -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO .whl -k "$CLOUDSMITH_API_KEY" -``` - -*** - - -### Recipe 17 - Tag-Based Exception Policy -Use this policy when you need a quick, time-boxed exception. For example, policy can quarantine by severity; where a separate terminal exemption policy matches if a package has an exempt tag and stops further evaluation.
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-17/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) - -cat < payload.json -{ - "name": "Tag based exception", - "description": "Exception policy based on basic tagging", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 17 -} -EOF -``` - -**Trade-off:** While the tag remains, new CVEs on that package won’t trigger quarantine. You'll need to review those tags regularly. - -*** - -### Recipe 18 - Exact allowlist exception policy with CVSS ceiling -Use when you want tight control per version, but still prevent exemptions if a CVSS exceeds a ceiling.
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-18/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) - -cat < payload.json -{ - "name": "Specific allowlist exception", - "description": "Controls based on exact version but still prevent exemptions if a CVSS exceeds a ceiling.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": false, - "precedence": 18 -} -EOF -``` - -**Trade-off:** While the tag remains, new CVEs on that package won’t trigger quarantine. You'll need to review those tags regularly. - -*** - -### Recipe 19 - Malware advisory -Use when you want to match based on malware advisory
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-19/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) - -cat < payload.json -{ - "name": "Malware", - "description": "Control malware ingestion.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": true, - "precedence": 1 -} -EOF -``` -*** - -### Recipe 20 - npm last published date -Use when you want to match based on the npm last published date on npm upstream
-Download the ```policy.rego``` and create the associated ```payload.json``` with the below command: -``` -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-20/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) - -cat < payload.json -{ - "name": "npm last published date on npm upstream", - "description": "Match if the publish date comes after the date of the set number of days ago.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": true, - "precedence": 1 -} -EOF -``` -*** - -### Recipe 21 - Exact blocklist - -This policy lets you maintain an **exact deny list** of suspicious or malicious packages across formats using the key pattern: - -`::` (for example: `npm:@alloc/quick-lru:5.2.0`, `python:requests:2.6.0`). - -It’s especially useful for incident response scenarios like Shai-Hulud-style attacks, where you receive a CSV or text list of impacted packages and need to block them immediately — even before upstream registries have removed them. - -Download the `policy.rego` and create the associated `payload.json` with: - -```bash -wget https://raw.githubusercontent.com/cloudsmith-io/rego-recipes/refs/heads/main/recipe-21/policy.rego -escaped_policy=$(jq -Rs . < policy.rego) -cat < payload.json -{ - "name": "Exact blocklist by format/name/version", - "description": "Blocks packages that appear on an external suspicious or malicious list, using exact format:name:version matches.", - "rego": $escaped_policy, - "enabled": true, - "is_terminal": true, - "precedence": 1 -} -EOF - -### Hugging Face recipes - -For policies relating to Hugging Face models and datasets, see [Hugging Face Recipes](https://github.com/cloudsmith-io/rego-recipes/blob/main/huggingface-recipes/README.md/). + +This repository contains curated, production-ready Open Policy Agent (OPA) policies for use with Cloudsmith Enterprise Policy Management (EPM). + +The goal of this repository is to define a clear, recommended secure baseline for Cloudsmith workspaces, along with a smaller set of advanced governance patterns. + +--- + +## Design Principles + +All policies in this repository: + +- Are WASM-compatible +- Use only supported Cloudsmith EPM builtins +- Avoid deprecated syntax (e.g. `import rego.v1`) +- Follow OPA style guidelines +- Are structured for composability using precedence +- Are safe for production use + +These policies are intended to be readable, predictable, and suitable for enterprise environments. + +--- + +## Repository Structure +baseline/ +advanced/ +legacy/ + +### baseline/ + +Recommended secure defaults for production environments. + +These policies address common supply chain security requirements such as: + +- Malware blocking +- High-risk vulnerability control (CVSS / EPSS) +- License compliance +- Controlled cooldown workflows +- Explicit allowlist and blocklist handling + +If you are deploying EPM in a new workspace, start here. + +--- + +### advanced/ + +Optional or format-specific policies that provide deeper governance controls. + +These may include: + +- Base image origin enforcement +- SBOM-based controls +- Model governance policies +- Specialised workflow patterns + +Advanced policies are production-ready but not universally required. + +--- + +### legacy/ + +Historical recipes and experimental policies retained for reference. + +Policies in this directory: + +- May use older patterns +- May not reflect current schema or best practices +- Are not recommended for new deployments + +They are preserved for documentation history and migration reference. + +--- + +## Policy Ordering & Precedence + +Cloudsmith EPM evaluates policies in precedence order (lowest precedence runs first). + +All policies in this repository are designed to be non-terminal and composable. + +A recommended precedence pattern for baseline deployments is: + +1. Cooldown Restore (make eligible packages available again) +2. Cooldown (time-based quarantine) +3. License policy (tagging or governance) +4. High-risk vulnerability policy (quarantine based on thresholds) +5. Exact allowlist exemption (explicit override) +6. Malware block (final quarantine safeguard) + +All matched policy actions are applied within a single transaction. +The package state visible to users reflects the final committed result. + +For full EPM documentation, see: +https://docs.cloudsmith.com/supply-chain-security/epm + +--- + +## Deployment + +Policies can be deployed using the Cloudsmith API or CLI. + +Refer to the official documentation for EPM policy management and simulation: + +https://docs.cloudsmith.com/supply-chain-security/epm + +--- + +This repository is the single source of truth for: + +- Policy templates +- Documentation examples +- Secure baseline recommendations +- Enterprise EPM enablement guidance diff --git a/huggingface-recipes/README.md b/advanced/huggingface-recipes/README.md similarity index 100% rename from huggingface-recipes/README.md rename to advanced/huggingface-recipes/README.md diff --git a/huggingface-recipes/model_card.rego b/advanced/huggingface-recipes/model_card.rego similarity index 100% rename from huggingface-recipes/model_card.rego rename to advanced/huggingface-recipes/model_card.rego diff --git a/huggingface-recipes/risky_files.rego b/advanced/huggingface-recipes/risky_files.rego similarity index 100% rename from huggingface-recipes/risky_files.rego rename to advanced/huggingface-recipes/risky_files.rego diff --git a/huggingface-recipes/security_scan.rego b/advanced/huggingface-recipes/security_scan.rego similarity index 100% rename from huggingface-recipes/security_scan.rego rename to advanced/huggingface-recipes/security_scan.rego diff --git a/huggingface-recipes/trusted_publishers.rego b/advanced/huggingface-recipes/trusted_publishers.rego similarity index 100% rename from huggingface-recipes/trusted_publishers.rego rename to advanced/huggingface-recipes/trusted_publishers.rego diff --git a/baseline/cooldown-restore.rego b/baseline/cooldown-restore.rego new file mode 100644 index 0000000..da6ba82 --- /dev/null +++ b/baseline/cooldown-restore.rego @@ -0,0 +1,27 @@ +package cloudsmith + +default match := false + +pkg := input.v0.package + +within_past_days := 4 +required_tag := "cooldown" + +match if count(reason) > 0 + +reason[msg] if { + pkg.upstream_metadata.published_at != null + + some t in pkg.tags + t.name == required_tag + + publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) + cutoff := time.add_date(time.now_ns(), 0, 0, 0 - within_past_days) + + publish_date < cutoff + + msg := sprintf( + "Package older than %v days and tagged '%s' — restoring availability", + [within_past_days, required_tag], + ) +} diff --git a/baseline/cooldown.rego b/baseline/cooldown.rego new file mode 100644 index 0000000..563bd96 --- /dev/null +++ b/baseline/cooldown.rego @@ -0,0 +1,23 @@ +package cloudsmith + +default match := false + +pkg := input.v0.package + +within_past_days := 3 + +match if count(reason) > 0 + +reason[msg] if { + pkg.upstream_metadata.published_at != null + + publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) + cutoff := time.add_date(time.now_ns(), 0, 0, 0 - within_past_days) + + publish_date >= cutoff + + msg := sprintf( + "Package published within last %v days — applying cooldown", + [within_past_days], + ) +} diff --git a/baseline/exact-allowlist-exemption.rego b/baseline/exact-allowlist-exemption.rego new file mode 100644 index 0000000..5a8ba87 --- /dev/null +++ b/baseline/exact-allowlist-exemption.rego @@ -0,0 +1,29 @@ +package cloudsmith + +default match := false + +# +# Explicit exemption allowlist +# Format: "::" +# + +allowlist := { + "python:example-lib:1.2.3", + "npm:example-ui:4.5.6", +} + +pkg := input.v0.package + +pkg_key := sprintf("%s:%s:%s", [pkg.format, pkg.name, pkg.version]) + +match if { + pkg_key in allowlist +} + +reason[msg] if { + match + msg := sprintf( + "Explicit exemption approved: %s", + [pkg_key], + ) +} diff --git a/baseline/exact-blocklist.rego b/baseline/exact-blocklist.rego new file mode 100644 index 0000000..ab1574b --- /dev/null +++ b/baseline/exact-blocklist.rego @@ -0,0 +1,29 @@ +package cloudsmith + +default match := false + +# +# Explicit blocklist +# Format: "::" +# + +blocklist := { + "python:malicious-lib:0.1.0", + "npm:compromised-ui:9.9.9", +} + +pkg := input.v0.package + +pkg_key := sprintf("%s:%s:%s", [pkg.format, pkg.name, pkg.version]) + +match if { + pkg_key in blocklist +} + +reason[msg] if { + match + msg := sprintf( + "Blocked by explicit deny list: %s", + [pkg_key], + ) +} diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego new file mode 100644 index 0000000..2d5cc5f --- /dev/null +++ b/baseline/high-risk-vulnerability.rego @@ -0,0 +1,134 @@ +package cloudsmith + +# ----------------------------------------------------------------------------- +# This policy blocks vulnerabilities based on both severity and exploitability: +# +# • CVSS ≥ 9.0 → always block (critical severity) +# • CVSS ≥ 7.0 AND EPSS ≥ 0.1 → block if likely to be exploited +# +# EPSS represents the probability of real-world exploitation, helping focus on +# vulnerabilities that attackers are actively targeting. +# +# Only vulnerabilities with a fix available are blocked. +# +# ----------------------------------------------------------------------------- +# + +default match := false + +# +# POLICY THRESHOLDS +# +# Critical severity: always block +critical_cvss := 9.0 + +# High severity: block if also likely exploited +high_cvss := 7.0 + +# EPSS exploit probability threshold +# 0.1 = 10% probability of exploitation in wild +min_epss := 0.1 + +# +# OPTIONAL: explicitly ignored CVEs +# +ignored_cves := { + # Example: + # "CVE-2023-12345" +} + +# +# MAIN MATCH CONDITION +# +match if { + some vuln in input.v0.vulnerabilities + + not ignored(vuln) + has_fix(vuln) + + should_block(vuln) +} + +# +# BLOCKING LOGIC +# +should_block(vuln) if { + critical_severity(vuln) +} + +should_block(vuln) if { + exploitable_high_severity(vuln) +} + +# +# CRITICAL: CVSS >= 9 (always block) +# +critical_severity(vuln) if { + some _, score in vuln.cvss + score.V3Score >= critical_cvss +} + +# +# HIGH + EXPLOITED: CVSS >= 7 AND EPSS >= threshold +# +exploitable_high_severity(vuln) if { + some _, score in vuln.cvss + + score.V3Score >= high_cvss + + vuln.epss.score != null + to_number(vuln.epss.score) >= min_epss +} + +# +# ONLY BLOCK IF FIX EXISTS +# +has_fix(vuln) if { + vuln.patched_versions + count(vuln.patched_versions) > 0 +} + +# +# IGNORE SPECIFIC CVES +# +ignored(vuln) if { + vuln.identifier in ignored_cves +} + +# +# OPTIONAL: HUMAN-READABLE REASON +# +reason[msg] if { + some vuln in input.v0.vulnerabilities + + not ignored(vuln) + has_fix(vuln) + + critical_severity(vuln) + + msg := sprintf( + "Blocking %v: Critical vulnerability (CVSS %.1f)", + [ + vuln.identifier, + vuln.cvss[_].V3Score + ] + ) +} + +reason[msg] if { + some vuln in input.v0.vulnerabilities + + not ignored(vuln) + has_fix(vuln) + + exploitable_high_severity(vuln) + + msg := sprintf( + "Blocking %v: High severity (CVSS %.1f) and actively exploited (EPSS %.3f)", + [ + vuln.identifier, + vuln.cvss[_].V3Score, + to_number(vuln.epss.score) + ] + ) +} \ No newline at end of file diff --git a/baseline/license-compliance.rego b/baseline/license-compliance.rego new file mode 100644 index 0000000..7903e7c --- /dev/null +++ b/baseline/license-compliance.rego @@ -0,0 +1,118 @@ +package cloudsmith + +default match := false + +# GNU General Public License (GPL) variants +gpl_licenses := { + "GPL-1.0-only", + "GPL-1.0-or-later", + "GPL-2.0", + "GPL-2.0-only", + "GPL-2.0-or-later", + "GPL-3.0", + "GPL-3.0-only", + "GPL-3.0-or-later", +} + +# GNU Lesser General Public License (LGPL) variants +lgpl_licenses := { + "LGPL-2.0", + "LGPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1", + "LGPL-2.1-only", + "LGPL-2.1-or-later", + "LGPL-3.0", + "LGPL-3.0-only", + "LGPL-3.0-or-later", +} + +# GNU Affero General Public License (AGPL) variants +agpl_licenses := { + "AGPL-1.0", + "AGPL-1.0-only", + "AGPL-1.0-or-later", + "AGPL-3.0", + "AGPL-3.0-only", + "AGPL-3.0-or-later", +} + +# Mozilla Public License (MPL) variants +mpl_licenses := { + "MPL-1.0", + "MPL-1.1", + "MPL-2.0", +} + +# Common Development and Distribution License (CDDL) variants +cddl_licenses := { + "CDDL-1.0", + "CDDL-1.1", +} + +# Eclipse Public License (EPL) variants +epl_licenses := { + "EPL-1.0", + "EPL-2.0", +} + +# Open Software License (OSL) variants +osl_licenses := { + "OSL-1.0", + "OSL-2.0", + "OSL-3.0", +} + +# GNU Free Documentation License (GFDL) variants +gfdl_licenses := { + "GFDL-1.1-only", + "GFDL-1.1-or-later", + "GFDL-1.2-only", + "GFDL-1.2-or-later", + "GFDL-1.3-only", + "GFDL-1.3-or-later", +} + +# Creative Commons Share Alike (CC-BY-SA) variants +cc_by_sa_licenses := { + "CC-BY-SA-1.0", + "CC-BY-SA-2.0", + "CC-BY-SA-2.5", + "CC-BY-SA-3.0", + "CC-BY-SA-4.0", +} + +# Other copyleft licenses +other_copyleft_licenses := { + "QPL-1.0", + "Sleepycat", + "SSPL-1.0", + "copyleft-next-0.3.0", +} + +# Combined copyleft license set +copyleft := gpl_licenses + | lgpl_licenses + | agpl_licenses + | mpl_licenses + | cddl_licenses + | epl_licenses + | osl_licenses + | gfdl_licenses + | cc_by_sa_licenses + | other_copyleft_licenses + +# Main policy rule +match if { + input.v0.package.license.oss_license.spdx_identifier in copyleft +} + +# Explanation for decision logs / error text +reason contains msg if { + match + lic := input.v0.package.license.oss_license.spdx_identifier + msg := sprintf( + "Copyleft license detected (%s). Package blocked/quarantined per license policy.", + [lic], + ) +} \ No newline at end of file diff --git a/baseline/malware-block.rego b/baseline/malware-block.rego new file mode 100644 index 0000000..c20e7ca --- /dev/null +++ b/baseline/malware-block.rego @@ -0,0 +1,18 @@ +package cloudsmith + +default match := false + +match if count(malicious_packages) > 0 + +malicious_packages := [vulnerability.id | + some vulnerability in input.v0.osv + startswith(vulnerability.id, "MAL-") +] + +reason contains msg if { + match + msg := sprintf( + "Detected %d malicious vulnerability ID(s): %v", + [count(malicious_packages), malicious_packages], + ) +} \ No newline at end of file diff --git a/recipe-1/policy.rego b/legacy/recipe-1/policy.rego similarity index 100% rename from recipe-1/policy.rego rename to legacy/recipe-1/policy.rego diff --git a/recipe-10/policy.rego b/legacy/recipe-10/policy.rego similarity index 100% rename from recipe-10/policy.rego rename to legacy/recipe-10/policy.rego diff --git a/recipe-11/policy.rego b/legacy/recipe-11/policy.rego similarity index 100% rename from recipe-11/policy.rego rename to legacy/recipe-11/policy.rego diff --git a/recipe-12/policy.rego b/legacy/recipe-12/policy.rego similarity index 100% rename from recipe-12/policy.rego rename to legacy/recipe-12/policy.rego diff --git a/recipe-13/policy.rego b/legacy/recipe-13/policy.rego similarity index 100% rename from recipe-13/policy.rego rename to legacy/recipe-13/policy.rego diff --git a/recipe-14/policy.rego b/legacy/recipe-14/policy.rego similarity index 100% rename from recipe-14/policy.rego rename to legacy/recipe-14/policy.rego diff --git a/recipe-15/policy.rego b/legacy/recipe-15/policy.rego similarity index 100% rename from recipe-15/policy.rego rename to legacy/recipe-15/policy.rego diff --git a/recipe-16/policy.rego b/legacy/recipe-16/policy.rego similarity index 100% rename from recipe-16/policy.rego rename to legacy/recipe-16/policy.rego diff --git a/recipe-17/policy.rego b/legacy/recipe-17/policy.rego similarity index 100% rename from recipe-17/policy.rego rename to legacy/recipe-17/policy.rego diff --git a/recipe-18/policy.rego b/legacy/recipe-18/policy.rego similarity index 100% rename from recipe-18/policy.rego rename to legacy/recipe-18/policy.rego diff --git a/recipe-19/policy.rego b/legacy/recipe-19/policy.rego similarity index 100% rename from recipe-19/policy.rego rename to legacy/recipe-19/policy.rego diff --git a/recipe-2/policy.rego b/legacy/recipe-2/policy.rego similarity index 100% rename from recipe-2/policy.rego rename to legacy/recipe-2/policy.rego diff --git a/recipe-20/policy.rego b/legacy/recipe-20/policy.rego similarity index 100% rename from recipe-20/policy.rego rename to legacy/recipe-20/policy.rego diff --git a/recipe-21/policy.rego b/legacy/recipe-21/policy.rego similarity index 100% rename from recipe-21/policy.rego rename to legacy/recipe-21/policy.rego diff --git a/recipe-3/policy.rego b/legacy/recipe-3/policy.rego similarity index 100% rename from recipe-3/policy.rego rename to legacy/recipe-3/policy.rego diff --git a/recipe-4/policy.rego b/legacy/recipe-4/policy.rego similarity index 100% rename from recipe-4/policy.rego rename to legacy/recipe-4/policy.rego diff --git a/recipe-5/policy.rego b/legacy/recipe-5/policy.rego similarity index 100% rename from recipe-5/policy.rego rename to legacy/recipe-5/policy.rego diff --git a/recipe-6/policy.rego b/legacy/recipe-6/policy.rego similarity index 100% rename from recipe-6/policy.rego rename to legacy/recipe-6/policy.rego diff --git a/recipe-7/policy.rego b/legacy/recipe-7/policy.rego similarity index 100% rename from recipe-7/policy.rego rename to legacy/recipe-7/policy.rego diff --git a/recipe-8/policy.rego b/legacy/recipe-8/policy.rego similarity index 100% rename from recipe-8/policy.rego rename to legacy/recipe-8/policy.rego diff --git a/recipe-9/policy.rego b/legacy/recipe-9/policy.rego similarity index 100% rename from recipe-9/policy.rego rename to legacy/recipe-9/policy.rego From b82b6e3d194eca0970365c4186ff19381a9ace3a Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:47:36 +0000 Subject: [PATCH 02/35] Add OPA test --- .github/opa-lint.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/opa-lint.yml diff --git a/.github/opa-lint.yml b/.github/opa-lint.yml new file mode 100644 index 0000000..c8ee442 --- /dev/null +++ b/.github/opa-lint.yml @@ -0,0 +1,35 @@ +name: OPA Lint & Validate + +on: + pull_request: + push: + branches: [main] + +jobs: + opa: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install OPA + run: | + curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static + chmod +x opa + sudo mv opa /usr/local/bin/ + + - name: Show OPA Version + run: opa version + + - name: Format Check + run: | + [ -d baseline ] && opa fmt --fail baseline || echo "No baseline folder" + [ -d advanced ] && opa fmt --fail advanced || echo "No advanced folder" + + - name: Validate Policies + run: | + find baseline advanced -name "*.rego" -print0 | \ + while IFS= read -r -d '' file; do + echo "Checking $file" + opa check "$file" + done From ddfb6979f992a46cb3f2181d7de9858dea617b89 Mon Sep 17 00:00:00 2001 From: Ciara Carey Date: Thu, 19 Feb 2026 09:51:41 +0000 Subject: [PATCH 03/35] fix formatting opa errors --- advanced/huggingface-recipes/model_card.rego | 6 +- advanced/huggingface-recipes/risky_files.rego | 12 +- .../huggingface-recipes/security_scan.rego | 10 +- .../trusted_publishers.rego | 6 +- baseline/cooldown-restore.rego | 20 +-- baseline/cooldown.rego | 16 +-- baseline/exact-allowlist-exemption.rego | 16 +-- baseline/exact-blocklist.rego | 16 +-- baseline/high-risk-vulnerability.rego | 92 ++++++------- baseline/license-compliance.rego | 123 ++++++++---------- baseline/malware-block.rego | 16 +-- 11 files changed, 162 insertions(+), 171 deletions(-) diff --git a/advanced/huggingface-recipes/model_card.rego b/advanced/huggingface-recipes/model_card.rego index 3d412fb..fd34be9 100644 --- a/advanced/huggingface-recipes/model_card.rego +++ b/advanced/huggingface-recipes/model_card.rego @@ -9,6 +9,6 @@ pkg := input.v0.package hf_pkg if "huggingface" == pkg.format match if { - hf_pkg - "HuggingFaceTB/smollm-corpus" in pkg.card.datasets -} + hf_pkg + "HuggingFaceTB/smollm-corpus" in pkg.card.datasets +} diff --git a/advanced/huggingface-recipes/risky_files.rego b/advanced/huggingface-recipes/risky_files.rego index fce69a8..75e8dd8 100644 --- a/advanced/huggingface-recipes/risky_files.rego +++ b/advanced/huggingface-recipes/risky_files.rego @@ -22,11 +22,11 @@ is_upstream_pkg if input.v0.package.uploader.slug == "cloudsmith-o6v" # SavedModel (.pb) # GGUF (.gguf) -risky_file_extensions := {".h5", ".hdf5", ".pdparams", ".keras", ".bin", ".pkl", ".dat", ".pt", ".pth", ".ckpt", ".npy", ".joblib", ".dill", ".pb", ".gguf", ".zip",} +risky_file_extensions := {".h5", ".hdf5", ".pdparams", ".keras", ".bin", ".pkl", ".dat", ".pt", ".pth", ".ckpt", ".npy", ".joblib", ".dill", ".pb", ".gguf", ".zip"} match if { - hf_pkg - is_upstream_pkg - some file in pkg.files - file.file_extension in risky_file_extensions -} \ No newline at end of file + hf_pkg + is_upstream_pkg + some file in pkg.files + file.file_extension in risky_file_extensions +} diff --git a/advanced/huggingface-recipes/security_scan.rego b/advanced/huggingface-recipes/security_scan.rego index b239629..9a42e65 100644 --- a/advanced/huggingface-recipes/security_scan.rego +++ b/advanced/huggingface-recipes/security_scan.rego @@ -11,15 +11,15 @@ is_upstream_pkg if input.v0.package.uploader.slug == "cloudsmith-o6v" # find any problematic content. # Users an incremental rule to express OR. incomplete_or_unsafe if { - input.v0.model_security.availability != "COMPLETE" + input.v0.model_security.availability != "COMPLETE" } incomplete_or_unsafe if { - input.v0.model_security.scan_summary != "SAFE" + input.v0.model_security.scan_summary != "SAFE" } match if { - "huggingface" == input.v0.package.format - is_upstream_pkg - incomplete_or_unsafe + "huggingface" == input.v0.package.format + is_upstream_pkg + incomplete_or_unsafe } diff --git a/advanced/huggingface-recipes/trusted_publishers.rego b/advanced/huggingface-recipes/trusted_publishers.rego index dc05d6c..f7a44c6 100644 --- a/advanced/huggingface-recipes/trusted_publishers.rego +++ b/advanced/huggingface-recipes/trusted_publishers.rego @@ -12,7 +12,7 @@ verified_publishers := {"amazon", "apple", "facebook", "FacebookAI", "google", " publisher := split(input.v0.package.name, "/")[0] match if { - "huggingface" == input.v0.package.format - is_upstream_pkg - publisher in verified_publishers + "huggingface" == input.v0.package.format + is_upstream_pkg + publisher in verified_publishers } diff --git a/baseline/cooldown-restore.rego b/baseline/cooldown-restore.rego index da6ba82..700bd07 100644 --- a/baseline/cooldown-restore.rego +++ b/baseline/cooldown-restore.rego @@ -10,18 +10,18 @@ required_tag := "cooldown" match if count(reason) > 0 reason[msg] if { - pkg.upstream_metadata.published_at != null + pkg.upstream_metadata.published_at != null - some t in pkg.tags - t.name == required_tag + some t in pkg.tags + t.name == required_tag - publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) - cutoff := time.add_date(time.now_ns(), 0, 0, 0 - within_past_days) + publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) + cutoff := time.add_date(time.now_ns(), 0, 0, 0 - within_past_days) - publish_date < cutoff + publish_date < cutoff - msg := sprintf( - "Package older than %v days and tagged '%s' — restoring availability", - [within_past_days, required_tag], - ) + msg := sprintf( + "Package older than %v days and tagged '%s' — restoring availability", + [within_past_days, required_tag], + ) } diff --git a/baseline/cooldown.rego b/baseline/cooldown.rego index 563bd96..218ed7b 100644 --- a/baseline/cooldown.rego +++ b/baseline/cooldown.rego @@ -9,15 +9,15 @@ within_past_days := 3 match if count(reason) > 0 reason[msg] if { - pkg.upstream_metadata.published_at != null + pkg.upstream_metadata.published_at != null - publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) - cutoff := time.add_date(time.now_ns(), 0, 0, 0 - within_past_days) + publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) + cutoff := time.add_date(time.now_ns(), 0, 0, 0 - within_past_days) - publish_date >= cutoff + publish_date >= cutoff - msg := sprintf( - "Package published within last %v days — applying cooldown", - [within_past_days], - ) + msg := sprintf( + "Package published within last %v days — applying cooldown", + [within_past_days], + ) } diff --git a/baseline/exact-allowlist-exemption.rego b/baseline/exact-allowlist-exemption.rego index 5a8ba87..d388317 100644 --- a/baseline/exact-allowlist-exemption.rego +++ b/baseline/exact-allowlist-exemption.rego @@ -8,8 +8,8 @@ default match := false # allowlist := { - "python:example-lib:1.2.3", - "npm:example-ui:4.5.6", + "python:example-lib:1.2.3", + "npm:example-ui:4.5.6", } pkg := input.v0.package @@ -17,13 +17,13 @@ pkg := input.v0.package pkg_key := sprintf("%s:%s:%s", [pkg.format, pkg.name, pkg.version]) match if { - pkg_key in allowlist + pkg_key in allowlist } reason[msg] if { - match - msg := sprintf( - "Explicit exemption approved: %s", - [pkg_key], - ) + match + msg := sprintf( + "Explicit exemption approved: %s", + [pkg_key], + ) } diff --git a/baseline/exact-blocklist.rego b/baseline/exact-blocklist.rego index ab1574b..cc19257 100644 --- a/baseline/exact-blocklist.rego +++ b/baseline/exact-blocklist.rego @@ -8,8 +8,8 @@ default match := false # blocklist := { - "python:malicious-lib:0.1.0", - "npm:compromised-ui:9.9.9", + "python:malicious-lib:0.1.0", + "npm:compromised-ui:9.9.9", } pkg := input.v0.package @@ -17,13 +17,13 @@ pkg := input.v0.package pkg_key := sprintf("%s:%s:%s", [pkg.format, pkg.name, pkg.version]) match if { - pkg_key in blocklist + pkg_key in blocklist } reason[msg] if { - match - msg := sprintf( - "Blocked by explicit deny list: %s", - [pkg_key], - ) + match + msg := sprintf( + "Blocked by explicit deny list: %s", + [pkg_key], + ) } diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index 2d5cc5f..6c72db3 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -32,103 +32,103 @@ min_epss := 0.1 # # OPTIONAL: explicitly ignored CVEs # -ignored_cves := { - # Example: - # "CVE-2023-12345" -} +ignored_cves := {} + +# Example: +# "CVE-2023-12345" # # MAIN MATCH CONDITION # match if { - some vuln in input.v0.vulnerabilities + some vuln in input.v0.vulnerabilities - not ignored(vuln) - has_fix(vuln) + not ignored(vuln) + has_fix(vuln) - should_block(vuln) + should_block(vuln) } # # BLOCKING LOGIC # should_block(vuln) if { - critical_severity(vuln) + critical_severity(vuln) } should_block(vuln) if { - exploitable_high_severity(vuln) + exploitable_high_severity(vuln) } # # CRITICAL: CVSS >= 9 (always block) # critical_severity(vuln) if { - some _, score in vuln.cvss - score.V3Score >= critical_cvss + some _, score in vuln.cvss + score.V3Score >= critical_cvss } # # HIGH + EXPLOITED: CVSS >= 7 AND EPSS >= threshold # exploitable_high_severity(vuln) if { - some _, score in vuln.cvss + some _, score in vuln.cvss - score.V3Score >= high_cvss + score.V3Score >= high_cvss - vuln.epss.score != null - to_number(vuln.epss.score) >= min_epss + vuln.epss.score != null + to_number(vuln.epss.score) >= min_epss } # # ONLY BLOCK IF FIX EXISTS # has_fix(vuln) if { - vuln.patched_versions - count(vuln.patched_versions) > 0 + vuln.patched_versions + count(vuln.patched_versions) > 0 } # # IGNORE SPECIFIC CVES # ignored(vuln) if { - vuln.identifier in ignored_cves + vuln.identifier in ignored_cves } # # OPTIONAL: HUMAN-READABLE REASON # reason[msg] if { - some vuln in input.v0.vulnerabilities + some vuln in input.v0.vulnerabilities - not ignored(vuln) - has_fix(vuln) + not ignored(vuln) + has_fix(vuln) - critical_severity(vuln) + critical_severity(vuln) - msg := sprintf( - "Blocking %v: Critical vulnerability (CVSS %.1f)", - [ - vuln.identifier, - vuln.cvss[_].V3Score - ] - ) + msg := sprintf( + "Blocking %v: Critical vulnerability (CVSS %.1f)", + [ + vuln.identifier, + vuln.cvss[_].V3Score, + ], + ) } reason[msg] if { - some vuln in input.v0.vulnerabilities - - not ignored(vuln) - has_fix(vuln) - - exploitable_high_severity(vuln) - - msg := sprintf( - "Blocking %v: High severity (CVSS %.1f) and actively exploited (EPSS %.3f)", - [ - vuln.identifier, - vuln.cvss[_].V3Score, - to_number(vuln.epss.score) - ] - ) -} \ No newline at end of file + some vuln in input.v0.vulnerabilities + + not ignored(vuln) + has_fix(vuln) + + exploitable_high_severity(vuln) + + msg := sprintf( + "Blocking %v: High severity (CVSS %.1f) and actively exploited (EPSS %.3f)", + [ + vuln.identifier, + vuln.cvss[_].V3Score, + to_number(vuln.epss.score), + ], + ) +} diff --git a/baseline/license-compliance.rego b/baseline/license-compliance.rego index 7903e7c..1355c29 100644 --- a/baseline/license-compliance.rego +++ b/baseline/license-compliance.rego @@ -4,115 +4,106 @@ default match := false # GNU General Public License (GPL) variants gpl_licenses := { - "GPL-1.0-only", - "GPL-1.0-or-later", - "GPL-2.0", - "GPL-2.0-only", - "GPL-2.0-or-later", - "GPL-3.0", - "GPL-3.0-only", - "GPL-3.0-or-later", + "GPL-1.0-only", + "GPL-1.0-or-later", + "GPL-2.0", + "GPL-2.0-only", + "GPL-2.0-or-later", + "GPL-3.0", + "GPL-3.0-only", + "GPL-3.0-or-later", } # GNU Lesser General Public License (LGPL) variants lgpl_licenses := { - "LGPL-2.0", - "LGPL-2.0-only", - "LGPL-2.0-or-later", - "LGPL-2.1", - "LGPL-2.1-only", - "LGPL-2.1-or-later", - "LGPL-3.0", - "LGPL-3.0-only", - "LGPL-3.0-or-later", + "LGPL-2.0", + "LGPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1", + "LGPL-2.1-only", + "LGPL-2.1-or-later", + "LGPL-3.0", + "LGPL-3.0-only", + "LGPL-3.0-or-later", } # GNU Affero General Public License (AGPL) variants agpl_licenses := { - "AGPL-1.0", - "AGPL-1.0-only", - "AGPL-1.0-or-later", - "AGPL-3.0", - "AGPL-3.0-only", - "AGPL-3.0-or-later", + "AGPL-1.0", + "AGPL-1.0-only", + "AGPL-1.0-or-later", + "AGPL-3.0", + "AGPL-3.0-only", + "AGPL-3.0-or-later", } # Mozilla Public License (MPL) variants mpl_licenses := { - "MPL-1.0", - "MPL-1.1", - "MPL-2.0", + "MPL-1.0", + "MPL-1.1", + "MPL-2.0", } # Common Development and Distribution License (CDDL) variants cddl_licenses := { - "CDDL-1.0", - "CDDL-1.1", + "CDDL-1.0", + "CDDL-1.1", } # Eclipse Public License (EPL) variants epl_licenses := { - "EPL-1.0", - "EPL-2.0", + "EPL-1.0", + "EPL-2.0", } # Open Software License (OSL) variants osl_licenses := { - "OSL-1.0", - "OSL-2.0", - "OSL-3.0", + "OSL-1.0", + "OSL-2.0", + "OSL-3.0", } # GNU Free Documentation License (GFDL) variants gfdl_licenses := { - "GFDL-1.1-only", - "GFDL-1.1-or-later", - "GFDL-1.2-only", - "GFDL-1.2-or-later", - "GFDL-1.3-only", - "GFDL-1.3-or-later", + "GFDL-1.1-only", + "GFDL-1.1-or-later", + "GFDL-1.2-only", + "GFDL-1.2-or-later", + "GFDL-1.3-only", + "GFDL-1.3-or-later", } # Creative Commons Share Alike (CC-BY-SA) variants cc_by_sa_licenses := { - "CC-BY-SA-1.0", - "CC-BY-SA-2.0", - "CC-BY-SA-2.5", - "CC-BY-SA-3.0", - "CC-BY-SA-4.0", + "CC-BY-SA-1.0", + "CC-BY-SA-2.0", + "CC-BY-SA-2.5", + "CC-BY-SA-3.0", + "CC-BY-SA-4.0", } # Other copyleft licenses other_copyleft_licenses := { - "QPL-1.0", - "Sleepycat", - "SSPL-1.0", - "copyleft-next-0.3.0", + "QPL-1.0", + "Sleepycat", + "SSPL-1.0", + "copyleft-next-0.3.0", } # Combined copyleft license set -copyleft := gpl_licenses - | lgpl_licenses - | agpl_licenses - | mpl_licenses - | cddl_licenses - | epl_licenses - | osl_licenses - | gfdl_licenses - | cc_by_sa_licenses - | other_copyleft_licenses +copyleft := ((((((((gpl_licenses | lgpl_licenses) | agpl_licenses) | mpl_licenses) | cddl_licenses) | epl_licenses) | osl_licenses) | gfdl_licenses) | cc_by_sa_licenses) | other_copyleft_licenses # Main policy rule match if { - input.v0.package.license.oss_license.spdx_identifier in copyleft + input.v0.package.license.oss_license.spdx_identifier in copyleft } # Explanation for decision logs / error text reason contains msg if { - match - lic := input.v0.package.license.oss_license.spdx_identifier - msg := sprintf( - "Copyleft license detected (%s). Package blocked/quarantined per license policy.", - [lic], - ) -} \ No newline at end of file + match + lic := input.v0.package.license.oss_license.spdx_identifier + msg := sprintf( + "Copyleft license detected (%s). Package blocked/quarantined per license policy.", + [lic], + ) +} diff --git a/baseline/malware-block.rego b/baseline/malware-block.rego index c20e7ca..c9bd0d8 100644 --- a/baseline/malware-block.rego +++ b/baseline/malware-block.rego @@ -5,14 +5,14 @@ default match := false match if count(malicious_packages) > 0 malicious_packages := [vulnerability.id | - some vulnerability in input.v0.osv - startswith(vulnerability.id, "MAL-") + some vulnerability in input.v0.osv + startswith(vulnerability.id, "MAL-") ] reason contains msg if { - match - msg := sprintf( - "Detected %d malicious vulnerability ID(s): %v", - [count(malicious_packages), malicious_packages], - ) -} \ No newline at end of file + match + msg := sprintf( + "Detected %d malicious vulnerability ID(s): %v", + [count(malicious_packages), malicious_packages], + ) +} From 2747c245af6b749dd10c9b948aae1bf0a1eb2d2d Mon Sep 17 00:00:00 2001 From: Ciara Carey Date: Thu, 19 Feb 2026 10:26:48 +0000 Subject: [PATCH 04/35] Improving the rego and moving test to workflows --- .github/{ => workflows}/opa-lint.yml | 0 baseline/cooldown-restore.rego | 12 ++- baseline/cooldown.rego | 12 ++- baseline/exact-allowlist-exemption.rego | 10 +-- baseline/exact-blocklist.rego | 10 +-- baseline/high-risk-vulnerability.rego | 64 ++------------ baseline/license-compliance.rego | 110 ++++-------------------- baseline/malware-block.rego | 8 +- 8 files changed, 55 insertions(+), 171 deletions(-) rename .github/{ => workflows}/opa-lint.yml (100%) diff --git a/.github/opa-lint.yml b/.github/workflows/opa-lint.yml similarity index 100% rename from .github/opa-lint.yml rename to .github/workflows/opa-lint.yml diff --git a/baseline/cooldown-restore.rego b/baseline/cooldown-restore.rego index 700bd07..20e3f75 100644 --- a/baseline/cooldown-restore.rego +++ b/baseline/cooldown-restore.rego @@ -7,19 +7,25 @@ pkg := input.v0.package within_past_days := 4 required_tag := "cooldown" -match if count(reason) > 0 +match if restored -reason[msg] if { +restored if { + pkg.upstream_metadata != null pkg.upstream_metadata.published_at != null some t in pkg.tags t.name == required_tag publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) - cutoff := time.add_date(time.now_ns(), 0, 0, 0 - within_past_days) + + days_ago := 0 - within_past_days + cutoff := time.add_date(time.now_ns(), 0, 0, days_ago) publish_date < cutoff +} +reason[msg] if { + restored msg := sprintf( "Package older than %v days and tagged '%s' — restoring availability", [within_past_days, required_tag], diff --git a/baseline/cooldown.rego b/baseline/cooldown.rego index 218ed7b..033e376 100644 --- a/baseline/cooldown.rego +++ b/baseline/cooldown.rego @@ -6,16 +6,22 @@ pkg := input.v0.package within_past_days := 3 -match if count(reason) > 0 +match if cooldown_needed -reason[msg] if { +cooldown_needed if { + pkg.upstream_metadata != null pkg.upstream_metadata.published_at != null publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) - cutoff := time.add_date(time.now_ns(), 0, 0, 0 - within_past_days) + + days_ago := 0 - within_past_days + cutoff := time.add_date(time.now_ns(), 0, 0, days_ago) publish_date >= cutoff +} +reason[msg] if { + cooldown_needed msg := sprintf( "Package published within last %v days — applying cooldown", [within_past_days], diff --git a/baseline/exact-allowlist-exemption.rego b/baseline/exact-allowlist-exemption.rego index d388317..1932118 100644 --- a/baseline/exact-allowlist-exemption.rego +++ b/baseline/exact-allowlist-exemption.rego @@ -2,21 +2,19 @@ package cloudsmith default match := false -# -# Explicit exemption allowlist -# Format: "::" -# +pkg := input.v0.package allowlist := { "python:example-lib:1.2.3", "npm:example-ui:4.5.6", } -pkg := input.v0.package - pkg_key := sprintf("%s:%s:%s", [pkg.format, pkg.name, pkg.version]) match if { + pkg.format != null + pkg.name != null + pkg.version != null pkg_key in allowlist } diff --git a/baseline/exact-blocklist.rego b/baseline/exact-blocklist.rego index cc19257..2ffea4d 100644 --- a/baseline/exact-blocklist.rego +++ b/baseline/exact-blocklist.rego @@ -2,21 +2,19 @@ package cloudsmith default match := false -# -# Explicit blocklist -# Format: "::" -# +pkg := input.v0.package blocklist := { "python:malicious-lib:0.1.0", "npm:compromised-ui:9.9.9", } -pkg := input.v0.package - pkg_key := sprintf("%s:%s:%s", [pkg.format, pkg.name, pkg.version]) match if { + pkg.format != null + pkg.name != null + pkg.version != null pkg_key in blocklist } diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index 6c72db3..c4dabfd 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -1,57 +1,22 @@ package cloudsmith -# ----------------------------------------------------------------------------- -# This policy blocks vulnerabilities based on both severity and exploitability: -# -# • CVSS ≥ 9.0 → always block (critical severity) -# • CVSS ≥ 7.0 AND EPSS ≥ 0.1 → block if likely to be exploited -# -# EPSS represents the probability of real-world exploitation, helping focus on -# vulnerabilities that attackers are actively targeting. -# -# Only vulnerabilities with a fix available are blocked. -# -# ----------------------------------------------------------------------------- -# - default match := false -# -# POLICY THRESHOLDS -# -# Critical severity: always block critical_cvss := 9.0 - -# High severity: block if also likely exploited high_cvss := 7.0 - -# EPSS exploit probability threshold -# 0.1 = 10% probability of exploitation in wild min_epss := 0.1 -# -# OPTIONAL: explicitly ignored CVEs -# ignored_cves := {} -# Example: -# "CVE-2023-12345" - -# -# MAIN MATCH CONDITION -# match if { + input.v0.vulnerabilities != null some vuln in input.v0.vulnerabilities not ignored(vuln) has_fix(vuln) - should_block(vuln) } -# -# BLOCKING LOGIC -# should_block(vuln) if { critical_severity(vuln) } @@ -60,67 +25,48 @@ should_block(vuln) if { exploitable_high_severity(vuln) } -# -# CRITICAL: CVSS >= 9 (always block) -# critical_severity(vuln) if { some _, score in vuln.cvss score.V3Score >= critical_cvss } -# -# HIGH + EXPLOITED: CVSS >= 7 AND EPSS >= threshold -# exploitable_high_severity(vuln) if { some _, score in vuln.cvss - score.V3Score >= high_cvss - + vuln.epss != null vuln.epss.score != null to_number(vuln.epss.score) >= min_epss } -# -# ONLY BLOCK IF FIX EXISTS -# has_fix(vuln) if { vuln.patched_versions count(vuln.patched_versions) > 0 } -# -# IGNORE SPECIFIC CVES -# ignored(vuln) if { vuln.identifier in ignored_cves } -# -# OPTIONAL: HUMAN-READABLE REASON -# reason[msg] if { + input.v0.vulnerabilities != null some vuln in input.v0.vulnerabilities not ignored(vuln) has_fix(vuln) - critical_severity(vuln) msg := sprintf( "Blocking %v: Critical vulnerability (CVSS %.1f)", - [ - vuln.identifier, - vuln.cvss[_].V3Score, - ], + [vuln.identifier, vuln.cvss[_].V3Score], ) } reason[msg] if { + input.v0.vulnerabilities != null some vuln in input.v0.vulnerabilities not ignored(vuln) has_fix(vuln) - exploitable_high_severity(vuln) msg := sprintf( diff --git a/baseline/license-compliance.rego b/baseline/license-compliance.rego index 1355c29..e629aa5 100644 --- a/baseline/license-compliance.rego +++ b/baseline/license-compliance.rego @@ -2,104 +2,30 @@ package cloudsmith default match := false -# GNU General Public License (GPL) variants -gpl_licenses := { - "GPL-1.0-only", - "GPL-1.0-or-later", - "GPL-2.0", - "GPL-2.0-only", - "GPL-2.0-or-later", - "GPL-3.0", - "GPL-3.0-only", - "GPL-3.0-or-later", -} - -# GNU Lesser General Public License (LGPL) variants -lgpl_licenses := { - "LGPL-2.0", - "LGPL-2.0-only", - "LGPL-2.0-or-later", - "LGPL-2.1", - "LGPL-2.1-only", - "LGPL-2.1-or-later", - "LGPL-3.0", - "LGPL-3.0-only", - "LGPL-3.0-or-later", -} - -# GNU Affero General Public License (AGPL) variants -agpl_licenses := { - "AGPL-1.0", - "AGPL-1.0-only", - "AGPL-1.0-or-later", - "AGPL-3.0", - "AGPL-3.0-only", - "AGPL-3.0-or-later", -} - -# Mozilla Public License (MPL) variants -mpl_licenses := { - "MPL-1.0", - "MPL-1.1", - "MPL-2.0", -} - -# Common Development and Distribution License (CDDL) variants -cddl_licenses := { - "CDDL-1.0", - "CDDL-1.1", -} - -# Eclipse Public License (EPL) variants -epl_licenses := { - "EPL-1.0", - "EPL-2.0", -} - -# Open Software License (OSL) variants -osl_licenses := { - "OSL-1.0", - "OSL-2.0", - "OSL-3.0", -} - -# GNU Free Documentation License (GFDL) variants -gfdl_licenses := { - "GFDL-1.1-only", - "GFDL-1.1-or-later", - "GFDL-1.2-only", - "GFDL-1.2-or-later", - "GFDL-1.3-only", - "GFDL-1.3-or-later", -} - -# Creative Commons Share Alike (CC-BY-SA) variants -cc_by_sa_licenses := { - "CC-BY-SA-1.0", - "CC-BY-SA-2.0", - "CC-BY-SA-2.5", - "CC-BY-SA-3.0", - "CC-BY-SA-4.0", -} - -# Other copyleft licenses -other_copyleft_licenses := { - "QPL-1.0", - "Sleepycat", +copyleft := { + "GPL-1.0-only", "GPL-1.0-or-later", "GPL-2.0", "GPL-2.0-only", + "GPL-2.0-or-later", "GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later", + "LGPL-2.0", "LGPL-2.0-only", "LGPL-2.0-or-later", + "LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later", + "LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later", + "AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later", + "MPL-1.0", "MPL-1.1", "MPL-2.0", + "CDDL-1.0", "CDDL-1.1", + "EPL-1.0", "EPL-2.0", + "OSL-1.0", "OSL-2.0", "OSL-3.0", "SSPL-1.0", - "copyleft-next-0.3.0", } -# Combined copyleft license set -copyleft := ((((((((gpl_licenses | lgpl_licenses) | agpl_licenses) | mpl_licenses) | cddl_licenses) | epl_licenses) | osl_licenses) | gfdl_licenses) | cc_by_sa_licenses) | other_copyleft_licenses - -# Main policy rule match if { - input.v0.package.license.oss_license.spdx_identifier in copyleft + input.v0.package.license != null + input.v0.package.license.oss_license != null + + lic := input.v0.package.license.oss_license.spdx_identifier + lic != null + lic in copyleft } -# Explanation for decision logs / error text -reason contains msg if { +reason[msg] if { match lic := input.v0.package.license.oss_license.spdx_identifier msg := sprintf( diff --git a/baseline/malware-block.rego b/baseline/malware-block.rego index c9bd0d8..94a8d00 100644 --- a/baseline/malware-block.rego +++ b/baseline/malware-block.rego @@ -2,14 +2,18 @@ package cloudsmith default match := false -match if count(malicious_packages) > 0 +match if { + input.v0.osv != null + count(malicious_packages) > 0 +} malicious_packages := [vulnerability.id | + input.v0.osv != null some vulnerability in input.v0.osv startswith(vulnerability.id, "MAL-") ] -reason contains msg if { +reason[msg] if { match msg := sprintf( "Detected %d malicious vulnerability ID(s): %v", From 809db09f1837d49f50d4ea57a236b7c36c0262f0 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:02:37 +0000 Subject: [PATCH 05/35] Update opa-lint.yml --- .github/workflows/opa-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/opa-lint.yml b/.github/workflows/opa-lint.yml index c8ee442..92bfd4c 100644 --- a/.github/workflows/opa-lint.yml +++ b/.github/workflows/opa-lint.yml @@ -3,7 +3,7 @@ name: OPA Lint & Validate on: pull_request: push: - branches: [main] + branches: [epm-baseline-refactor] jobs: opa: From cbb09cdc36b63d17355f991b23154a367d498203 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:03:18 +0000 Subject: [PATCH 06/35] Refactor vulnerability matching and scoring logic --- baseline/high-risk-vulnerability.rego | 115 +++++++++++++++++--------- 1 file changed, 74 insertions(+), 41 deletions(-) diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index c4dabfd..87f9116 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -9,72 +9,105 @@ min_epss := 0.1 ignored_cves := {} match if { - input.v0.vulnerabilities != null - some vuln in input.v0.vulnerabilities + input.v0.vulnerabilities != null + some vuln in input.v0.vulnerabilities - not ignored(vuln) - has_fix(vuln) - should_block(vuln) + not ignored(vuln) + has_fix(vuln) + should_block(vuln) } should_block(vuln) if { - critical_severity(vuln) + critical_severity(vuln) } should_block(vuln) if { - exploitable_high_severity(vuln) + exploitable_high_severity(vuln) } critical_severity(vuln) if { - some _, score in vuln.cvss - score.V3Score >= critical_cvss + score := cvss_v3_score(vuln) + score >= critical_cvss } exploitable_high_severity(vuln) if { - some _, score in vuln.cvss - score.V3Score >= high_cvss - vuln.epss != null - vuln.epss.score != null - to_number(vuln.epss.score) >= min_epss + score := cvss_v3_score(vuln) + score >= high_cvss + + epss := epss_score(vuln) + epss >= min_epss } has_fix(vuln) if { - vuln.patched_versions - count(vuln.patched_versions) > 0 + vuln.patched_versions + count(vuln.patched_versions) > 0 } ignored(vuln) if { - vuln.identifier in ignored_cves + vuln.identifier in ignored_cves +} + +cvss_v3_score(vuln) := s if { + vuln.cvss != null + vuln.cvss.redhat != null + vuln.cvss.redhat.V3Score != null + s := vuln.cvss.redhat.V3Score +} else := s if { + vuln.cvss != null + vuln.cvss.ghsa != null + vuln.cvss.ghsa.V3Score != null + s := vuln.cvss.ghsa.V3Score +} else := s if { + vuln.cvss != null + vuln.cvss.nvd != null + vuln.cvss.nvd.V3Score != null + s := vuln.cvss.nvd.V3Score +} else := s if { + # fallback: first non-null V3Score found in any source + vuln.cvss != null + some _, src in vuln.cvss + src != null + src.V3Score != null + s := src.V3Score } +epss_score(vuln) := s if { + vuln.epss != null + vuln.epss.score != null + s := to_number(vuln.epss.score) +} else := 0.0 + reason[msg] if { - input.v0.vulnerabilities != null - some vuln in input.v0.vulnerabilities + input.v0.vulnerabilities != null + some vuln in input.v0.vulnerabilities + + not ignored(vuln) + has_fix(vuln) - not ignored(vuln) - has_fix(vuln) - critical_severity(vuln) + score := cvss_v3_score(vuln) + score >= critical_cvss - msg := sprintf( - "Blocking %v: Critical vulnerability (CVSS %.1f)", - [vuln.identifier, vuln.cvss[_].V3Score], - ) + msg := sprintf( + "Blocking %v: Critical vulnerability (CVSS %.1f)", + [vuln.identifier, score], + ) } reason[msg] if { - input.v0.vulnerabilities != null - some vuln in input.v0.vulnerabilities - - not ignored(vuln) - has_fix(vuln) - exploitable_high_severity(vuln) - - msg := sprintf( - "Blocking %v: High severity (CVSS %.1f) and actively exploited (EPSS %.3f)", - [ - vuln.identifier, - vuln.cvss[_].V3Score, - to_number(vuln.epss.score), - ], - ) + input.v0.vulnerabilities != null + some vuln in input.v0.vulnerabilities + + not ignored(vuln) + has_fix(vuln) + + score := cvss_v3_score(vuln) + score >= high_cvss + + epss := epss_score(vuln) + epss >= min_epss + + msg := sprintf( + "Blocking %v: High severity (CVSS %.1f) and actively exploited (EPSS %.3f)", + [vuln.identifier, score, epss], + ) } From f9d53384226c315bc32392afbebd9c999487c7d1 Mon Sep 17 00:00:00 2001 From: Ciara Carey Date: Wed, 25 Feb 2026 14:23:45 +0000 Subject: [PATCH 07/35] opa formatting fixes --- baseline/high-risk-vulnerability.rego | 118 +++++++++++++------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index 87f9116..2e1ab66 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -9,105 +9,105 @@ min_epss := 0.1 ignored_cves := {} match if { - input.v0.vulnerabilities != null - some vuln in input.v0.vulnerabilities + input.v0.vulnerabilities != null + some vuln in input.v0.vulnerabilities - not ignored(vuln) - has_fix(vuln) - should_block(vuln) + not ignored(vuln) + has_fix(vuln) + should_block(vuln) } should_block(vuln) if { - critical_severity(vuln) + critical_severity(vuln) } should_block(vuln) if { - exploitable_high_severity(vuln) + exploitable_high_severity(vuln) } critical_severity(vuln) if { - score := cvss_v3_score(vuln) - score >= critical_cvss + score := cvss_v3_score(vuln) + score >= critical_cvss } exploitable_high_severity(vuln) if { - score := cvss_v3_score(vuln) - score >= high_cvss + score := cvss_v3_score(vuln) + score >= high_cvss - epss := epss_score(vuln) - epss >= min_epss + epss := epss_score(vuln) + epss >= min_epss } has_fix(vuln) if { - vuln.patched_versions - count(vuln.patched_versions) > 0 + vuln.patched_versions + count(vuln.patched_versions) > 0 } ignored(vuln) if { - vuln.identifier in ignored_cves + vuln.identifier in ignored_cves } cvss_v3_score(vuln) := s if { - vuln.cvss != null - vuln.cvss.redhat != null - vuln.cvss.redhat.V3Score != null - s := vuln.cvss.redhat.V3Score + vuln.cvss != null + vuln.cvss.redhat != null + vuln.cvss.redhat.V3Score != null + s := vuln.cvss.redhat.V3Score } else := s if { - vuln.cvss != null - vuln.cvss.ghsa != null - vuln.cvss.ghsa.V3Score != null - s := vuln.cvss.ghsa.V3Score + vuln.cvss != null + vuln.cvss.ghsa != null + vuln.cvss.ghsa.V3Score != null + s := vuln.cvss.ghsa.V3Score } else := s if { - vuln.cvss != null - vuln.cvss.nvd != null - vuln.cvss.nvd.V3Score != null - s := vuln.cvss.nvd.V3Score + vuln.cvss != null + vuln.cvss.nvd != null + vuln.cvss.nvd.V3Score != null + s := vuln.cvss.nvd.V3Score } else := s if { - # fallback: first non-null V3Score found in any source - vuln.cvss != null - some _, src in vuln.cvss - src != null - src.V3Score != null - s := src.V3Score + # fallback: first non-null V3Score found in any source + vuln.cvss != null + some _, src in vuln.cvss + src != null + src.V3Score != null + s := src.V3Score } epss_score(vuln) := s if { - vuln.epss != null - vuln.epss.score != null - s := to_number(vuln.epss.score) + vuln.epss != null + vuln.epss.score != null + s := to_number(vuln.epss.score) } else := 0.0 reason[msg] if { - input.v0.vulnerabilities != null - some vuln in input.v0.vulnerabilities + input.v0.vulnerabilities != null + some vuln in input.v0.vulnerabilities - not ignored(vuln) - has_fix(vuln) + not ignored(vuln) + has_fix(vuln) - score := cvss_v3_score(vuln) - score >= critical_cvss + score := cvss_v3_score(vuln) + score >= critical_cvss - msg := sprintf( - "Blocking %v: Critical vulnerability (CVSS %.1f)", - [vuln.identifier, score], - ) + msg := sprintf( + "Blocking %v: Critical vulnerability (CVSS %.1f)", + [vuln.identifier, score], + ) } reason[msg] if { - input.v0.vulnerabilities != null - some vuln in input.v0.vulnerabilities + input.v0.vulnerabilities != null + some vuln in input.v0.vulnerabilities - not ignored(vuln) - has_fix(vuln) + not ignored(vuln) + has_fix(vuln) - score := cvss_v3_score(vuln) - score >= high_cvss + score := cvss_v3_score(vuln) + score >= high_cvss - epss := epss_score(vuln) - epss >= min_epss + epss := epss_score(vuln) + epss >= min_epss - msg := sprintf( - "Blocking %v: High severity (CVSS %.1f) and actively exploited (EPSS %.3f)", - [vuln.identifier, score, epss], - ) + msg := sprintf( + "Blocking %v: High severity (CVSS %.1f) and actively exploited (EPSS %.3f)", + [vuln.identifier, score, epss], + ) } From 8bde0eb6903402492bf2706b370df588af48db6e Mon Sep 17 00:00:00 2001 From: Ciara Carey Date: Wed, 4 Mar 2026 12:59:51 +0000 Subject: [PATCH 08/35] update vulnerability policy to use osv object --- baseline/high-risk-vulnerability.rego | 159 +++++++++++--------------- 1 file changed, 68 insertions(+), 91 deletions(-) diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index 2e1ab66..78484dc 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -2,112 +2,89 @@ package cloudsmith default match := false -critical_cvss := 9.0 -high_cvss := 7.0 -min_epss := 0.1 - -ignored_cves := {} - -match if { - input.v0.vulnerabilities != null - some vuln in input.v0.vulnerabilities - - not ignored(vuln) - has_fix(vuln) - should_block(vuln) +match if count(reason) > 0 + +min_cvss := 7.0 +ignored := {} + +# ── Blocking key set ────────────────────────────────────────────────────────── +# Vulnerabilities are only blocked when a fix is available. +# This prevents breaking builds with no recourse for the developer. +# To block regardless of fix, remove has_fix_for_key(k) from this set. +blocking_keys := {k | + v := input.v0.osv[_] + k := vuln_keys(v)[_] + not k in ignored + max_cvss_for_key(k) >= min_cvss + has_fix_for_key(k) } -should_block(vuln) if { - critical_severity(vuln) +# ── Key derivation ──────────────────────────────────────────────────────────── +vuln_keys(v) := {a | a := v.aliases[_]} if { + count(v.aliases) > 0 +} else := {v.id} + +# ── CVSS extraction ─────────────────────────────────────────────────────────── +# Check both top-level and per-affected severity — OSV records vary +cvss_scores_for_record(v) := scores if { + top_level := {s | + sev := v.severity[_] + sev.numerical_score != null + s := sev.numerical_score + } + affected_level := {s | + a := v.affected[_] + sev := a.severity[_] + sev.numerical_score != null + s := sev.numerical_score + } + scores := top_level | affected_level } -should_block(vuln) if { - exploitable_high_severity(vuln) -} +max_cvss_for_key(k) := s if { + scores := {s | + v := input.v0.osv[_] + k in vuln_keys(v) + s := cvss_scores_for_record(v)[_] + } + count(scores) > 0 + s := max(scores) +} else := 0.0 -critical_severity(vuln) if { - score := cvss_v3_score(vuln) - score >= critical_cvss +# ── Fix detection ───────────────────────────────────────────────────────────── +has_fix_for_key(k) if { + count(fixed_versions_for_key(k)) > 0 } -exploitable_high_severity(vuln) if { - score := cvss_v3_score(vuln) - score >= high_cvss - - epss := epss_score(vuln) - epss >= min_epss +fixed_versions_for_key(k) := {f | + v := input.v0.osv[_] + k in vuln_keys(v) + r := v.affected[_].ranges[_] + e := r.events[_] + f := e.fixed + include_fix(f) } -has_fix(vuln) if { - vuln.patched_versions - count(vuln.patched_versions) > 0 +include_fix(f) if { + semver.is_valid(f) + semver.is_valid(input.v0.package.version) + semver.compare(f, input.v0.package.version) > 0 } -ignored(vuln) if { - vuln.identifier in ignored_cves -} +include_fix(f) if not semver.is_valid(f) -cvss_v3_score(vuln) := s if { - vuln.cvss != null - vuln.cvss.redhat != null - vuln.cvss.redhat.V3Score != null - s := vuln.cvss.redhat.V3Score -} else := s if { - vuln.cvss != null - vuln.cvss.ghsa != null - vuln.cvss.ghsa.V3Score != null - s := vuln.cvss.ghsa.V3Score -} else := s if { - vuln.cvss != null - vuln.cvss.nvd != null - vuln.cvss.nvd.V3Score != null - s := vuln.cvss.nvd.V3Score -} else := s if { - # fallback: first non-null V3Score found in any source - vuln.cvss != null - some _, src in vuln.cvss - src != null - src.V3Score != null - s := src.V3Score +include_fix(f) if { + semver.is_valid(f) + not semver.is_valid(input.v0.package.version) } -epss_score(vuln) := s if { - vuln.epss != null - vuln.epss.score != null - s := to_number(vuln.epss.score) -} else := 0.0 - +# ── Reason messages ─────────────────────────────────────────────────────────── reason[msg] if { - input.v0.vulnerabilities != null - some vuln in input.v0.vulnerabilities - - not ignored(vuln) - has_fix(vuln) - - score := cvss_v3_score(vuln) - score >= critical_cvss - - msg := sprintf( - "Blocking %v: Critical vulnerability (CVSS %.1f)", - [vuln.identifier, score], - ) -} - -reason[msg] if { - input.v0.vulnerabilities != null - some vuln in input.v0.vulnerabilities - - not ignored(vuln) - has_fix(vuln) - - score := cvss_v3_score(vuln) - score >= high_cvss - - epss := epss_score(vuln) - epss >= min_epss - + k := blocking_keys[_] + score := max_cvss_for_key(k) + fixes := fixed_versions_for_key(k) msg := sprintf( - "Blocking %v: High severity (CVSS %.1f) and actively exploited (EPSS %.3f)", - [vuln.identifier, score, epss], + "Blocking %v: CVSS %.1f, fix(es) available: %v", + [k, score, concat(", ", fixes)], ) } From 952298640bc127522a77277d37c9c816f1868176 Mon Sep 17 00:00:00 2001 From: Ciara Carey Date: Wed, 4 Mar 2026 13:04:19 +0000 Subject: [PATCH 09/35] update vulnerability policy --- baseline/high-risk-vulnerability.rego | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index 78484dc..b16981f 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -41,14 +41,14 @@ cvss_scores_for_record(v) := scores if { scores := top_level | affected_level } -max_cvss_for_key(k) := s if { +max_cvss_for_key(k) := result if { scores := {s | v := input.v0.osv[_] k in vuln_keys(v) s := cvss_scores_for_record(v)[_] } count(scores) > 0 - s := max(scores) + result := max(scores) } else := 0.0 # ── Fix detection ───────────────────────────────────────────────────────────── From 6bfd1ac279d01b438e1606f3d19258b8789023fb Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:37:17 +0000 Subject: [PATCH 10/35] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e6a2819..fe9bfb7 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,8 @@ A recommended precedence pattern for baseline deployments is: 3. License policy (tagging or governance) 4. High-risk vulnerability policy (quarantine based on thresholds) 5. Exact allowlist exemption (explicit override) -6. Malware block (final quarantine safeguard) +6. Exact blocklist (explicit deny) +7. Malware block (final quarantine safeguard) All matched policy actions are applied within a single transaction. The package state visible to users reflects the final committed result. From c300edbc5cda468264726dcf2b61df974eec4c29 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:37:37 +0000 Subject: [PATCH 11/35] Update .github/workflows/opa-lint.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/opa-lint.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/opa-lint.yml b/.github/workflows/opa-lint.yml index 92bfd4c..f250690 100644 --- a/.github/workflows/opa-lint.yml +++ b/.github/workflows/opa-lint.yml @@ -23,8 +23,16 @@ jobs: - name: Format Check run: | - [ -d baseline ] && opa fmt --fail baseline || echo "No baseline folder" - [ -d advanced ] && opa fmt --fail advanced || echo "No advanced folder" + if [ -d baseline ]; then + opa fmt --fail baseline + else + echo "No baseline folder" + fi + if [ -d advanced ]; then + opa fmt --fail advanced + else + echo "No advanced folder" + fi - name: Validate Policies run: | From aebc6ce5b7eec28591af81536e2b0f1931b9f85e Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:37:57 +0000 Subject: [PATCH 12/35] Update .github/workflows/opa-lint.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/opa-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/opa-lint.yml b/.github/workflows/opa-lint.yml index f250690..724ba7f 100644 --- a/.github/workflows/opa-lint.yml +++ b/.github/workflows/opa-lint.yml @@ -3,7 +3,7 @@ name: OPA Lint & Validate on: pull_request: push: - branches: [epm-baseline-refactor] + branches: [main, epm-baseline-refactor] jobs: opa: From d1a8e18e5b419860a52835174bb5050b9e51da51 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:43:05 +0000 Subject: [PATCH 13/35] fix(readme): wrap repository structure in fenced code block (#16) * Initial plan * fix(readme): wrap repository structure in fenced code block Co-authored-by: ciaracarey <84123925+ciaracarey@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ciaracarey <84123925+ciaracarey@users.noreply.github.com> --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index fe9bfb7..9c8a7bc 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,12 @@ These policies are intended to be readable, predictable, and suitable for enterp --- ## Repository Structure + +``` baseline/ advanced/ legacy/ +``` ### baseline/ From 97fbe8f380d232b322486bac258c8bb4c4317262 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:43:41 +0000 Subject: [PATCH 14/35] Fix Americanized spelling in README (#17) * Initial plan * Fix spelling: "Specialised" -> "Specialized" in README.md Co-authored-by: ciaracarey <84123925+ciaracarey@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ciaracarey <84123925+ciaracarey@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c8a7bc..1cd0e79 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ These may include: - Base image origin enforcement - SBOM-based controls - Model governance policies -- Specialised workflow patterns +- Specialized workflow patterns Advanced policies are production-ready but not universally required. From 2c7cc2f943ae8bfc32de53ea195afc2671acc366 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:44:28 +0000 Subject: [PATCH 15/35] Remove `import rego.v1` from advanced/huggingface-recipes policies (#18) * Initial plan * Remove import rego.v1 from advanced/huggingface-recipes policies Co-authored-by: ciaracarey <84123925+ciaracarey@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ciaracarey <84123925+ciaracarey@users.noreply.github.com> --- advanced/huggingface-recipes/model_card.rego | 2 -- advanced/huggingface-recipes/risky_files.rego | 2 -- advanced/huggingface-recipes/security_scan.rego | 2 -- advanced/huggingface-recipes/trusted_publishers.rego | 2 -- 4 files changed, 8 deletions(-) diff --git a/advanced/huggingface-recipes/model_card.rego b/advanced/huggingface-recipes/model_card.rego index fd34be9..c34c280 100644 --- a/advanced/huggingface-recipes/model_card.rego +++ b/advanced/huggingface-recipes/model_card.rego @@ -1,7 +1,5 @@ package cloudsmith -import rego.v1 - default match := false pkg := input.v0.package diff --git a/advanced/huggingface-recipes/risky_files.rego b/advanced/huggingface-recipes/risky_files.rego index 75e8dd8..b06a02c 100644 --- a/advanced/huggingface-recipes/risky_files.rego +++ b/advanced/huggingface-recipes/risky_files.rego @@ -1,7 +1,5 @@ package cloudsmith -import rego.v1 - default match := false pkg := input.v0.package diff --git a/advanced/huggingface-recipes/security_scan.rego b/advanced/huggingface-recipes/security_scan.rego index 9a42e65..bfb8740 100644 --- a/advanced/huggingface-recipes/security_scan.rego +++ b/advanced/huggingface-recipes/security_scan.rego @@ -1,7 +1,5 @@ package cloudsmith -import rego.v1 - default match := false # Upstream packages are fetched by a system user diff --git a/advanced/huggingface-recipes/trusted_publishers.rego b/advanced/huggingface-recipes/trusted_publishers.rego index f7a44c6..767c6ef 100644 --- a/advanced/huggingface-recipes/trusted_publishers.rego +++ b/advanced/huggingface-recipes/trusted_publishers.rego @@ -1,7 +1,5 @@ package cloudsmith -import rego.v1 - default match := false # Upstream packages are fetched by a system user From 0df6c6f3f3e968d40ff1b0768c315f224b1b5355 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:41:15 +0000 Subject: [PATCH 16/35] change name from cooldown to pacage age --- README.md | 6 +++--- ...cooldown.rego => package-age-quarantine.rego} | 8 ++++---- ...own-restore.rego => package-age-restore.rego} | 16 ++++++---------- 3 files changed, 13 insertions(+), 17 deletions(-) rename baseline/{cooldown.rego => package-age-quarantine.rego} (71%) rename baseline/{cooldown-restore.rego => package-age-restore.rego} (60%) diff --git a/README.md b/README.md index 1cd0e79..4c5bfb1 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ These policies address common supply chain security requirements such as: - Malware blocking - High-risk vulnerability control (CVSS / EPSS) - License compliance -- Controlled cooldown workflows +- Workflows using package age - Explicit allowlist and blocklist handling If you are deploying EPM in a new workspace, start here. @@ -82,8 +82,8 @@ All policies in this repository are designed to be non-terminal and composable. A recommended precedence pattern for baseline deployments is: -1. Cooldown Restore (make eligible packages available again) -2. Cooldown (time-based quarantine) +1. Package age restore (make eligible packages available again) +2. Packge age quarantine (time-based quarantine) 3. License policy (tagging or governance) 4. High-risk vulnerability policy (quarantine based on thresholds) 5. Exact allowlist exemption (explicit override) diff --git a/baseline/cooldown.rego b/baseline/package-age-quarantine.rego similarity index 71% rename from baseline/cooldown.rego rename to baseline/package-age-quarantine.rego index 033e376..e4d33ea 100644 --- a/baseline/cooldown.rego +++ b/baseline/package-age-quarantine.rego @@ -6,9 +6,9 @@ pkg := input.v0.package within_past_days := 3 -match if cooldown_needed +match if below_minimum_release_age -cooldown_needed if { +below_minimum_release_age if { pkg.upstream_metadata != null pkg.upstream_metadata.published_at != null @@ -21,9 +21,9 @@ cooldown_needed if { } reason[msg] if { - cooldown_needed + below_minimum_release_age msg := sprintf( - "Package published within last %v days — applying cooldown", + "Package published within last %v days — below minimum release age", [within_past_days], ) } diff --git a/baseline/cooldown-restore.rego b/baseline/package-age-restore.rego similarity index 60% rename from baseline/cooldown-restore.rego rename to baseline/package-age-restore.rego index 20e3f75..0b30ce3 100644 --- a/baseline/cooldown-restore.rego +++ b/baseline/package-age-restore.rego @@ -4,18 +4,14 @@ default match := false pkg := input.v0.package -within_past_days := 4 -required_tag := "cooldown" +within_past_days := 3 -match if restored +match if above_minimum_release_age -restored if { +above_minimum_release_age if { pkg.upstream_metadata != null pkg.upstream_metadata.published_at != null - some t in pkg.tags - t.name == required_tag - publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) days_ago := 0 - within_past_days @@ -25,9 +21,9 @@ restored if { } reason[msg] if { - restored + above_minimum_release_age msg := sprintf( - "Package older than %v days and tagged '%s' — restoring availability", - [within_past_days, required_tag], + "Package older than %v days — meets minimum release age", + [within_past_days], ) } From a1e39c4a010f7a2c96ed5ffa0b716af632e19fdf Mon Sep 17 00:00:00 2001 From: Ciara Carey Date: Tue, 10 Mar 2026 23:26:37 +0000 Subject: [PATCH 17/35] Simplify vulnerability policy --- baseline/high-risk-vulnerability.rego | 88 ++++++++++----------------- 1 file changed, 31 insertions(+), 57 deletions(-) diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index b16981f..34d6363 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -1,90 +1,64 @@ +# title: High Risk Vulnerability +# description: Blocks packages with a CVSS score >= X where a fix is available. package cloudsmith default match := false -match if count(reason) > 0 - min_cvss := 7.0 + ignored := {} -# ── Blocking key set ────────────────────────────────────────────────────────── -# Vulnerabilities are only blocked when a fix is available. -# This prevents breaking builds with no recourse for the developer. -# To block regardless of fix, remove has_fix_for_key(k) from this set. +match if count(reason) > 0 + +vuln_keys(v) := {a | some a in v.aliases} if { + count(v.aliases) > 0 +} else := {v.id} + +aliased_records[k] := records if { + some v_seed in input.v0.osv + some k in vuln_keys(v_seed) + records := {v | some v in input.v0.osv; k in vuln_keys(v)} +} + blocking_keys := {k | - v := input.v0.osv[_] - k := vuln_keys(v)[_] + some k, _ in aliased_records not k in ignored max_cvss_for_key(k) >= min_cvss has_fix_for_key(k) } -# ── Key derivation ──────────────────────────────────────────────────────────── -vuln_keys(v) := {a | a := v.aliases[_]} if { - count(v.aliases) > 0 -} else := {v.id} - -# ── CVSS extraction ─────────────────────────────────────────────────────────── -# Check both top-level and per-affected severity — OSV records vary -cvss_scores_for_record(v) := scores if { +cvss_scores_for_record(v) := top_level | affected_level if { top_level := {s | - sev := v.severity[_] + some sev in v.severity sev.numerical_score != null s := sev.numerical_score } affected_level := {s | - a := v.affected[_] - sev := a.severity[_] + some a in v.affected + some sev in a.severity sev.numerical_score != null s := sev.numerical_score } - scores := top_level | affected_level } -max_cvss_for_key(k) := result if { +max_cvss_for_key(k) := max(scores) if { scores := {s | - v := input.v0.osv[_] - k in vuln_keys(v) - s := cvss_scores_for_record(v)[_] + some v in aliased_records[k] + some s in cvss_scores_for_record(v) } count(scores) > 0 - result := max(scores) } else := 0.0 -# ── Fix detection ───────────────────────────────────────────────────────────── has_fix_for_key(k) if { - count(fixed_versions_for_key(k)) > 0 -} - -fixed_versions_for_key(k) := {f | - v := input.v0.osv[_] - k in vuln_keys(v) - r := v.affected[_].ranges[_] - e := r.events[_] - f := e.fixed - include_fix(f) -} - -include_fix(f) if { - semver.is_valid(f) - semver.is_valid(input.v0.package.version) - semver.compare(f, input.v0.package.version) > 0 -} - -include_fix(f) if not semver.is_valid(f) - -include_fix(f) if { - semver.is_valid(f) - not semver.is_valid(input.v0.package.version) + some v in aliased_records[k] + some a in v.affected + some r in a.ranges + some e in r.events + e.fixed } -# ── Reason messages ─────────────────────────────────────────────────────────── -reason[msg] if { - k := blocking_keys[_] +reason contains msg if { + some k in blocking_keys score := max_cvss_for_key(k) - fixes := fixed_versions_for_key(k) - msg := sprintf( - "Blocking %v: CVSS %.1f, fix(es) available: %v", - [k, score, concat(", ", fixes)], - ) + msg := sprintf("Blocking %v: CVSS %.1f, fix available", [k, score]) } From fcdb54084506651c7ea63fe6027f0bb734987b8c Mon Sep 17 00:00:00 2001 From: Ciara Carey Date: Tue, 10 Mar 2026 23:54:21 +0000 Subject: [PATCH 18/35] adding the GitOps exemption workflow --- .github/workflows/apply-exemptions.yml | 39 +++++ .gitignore | 208 ++++++++++++++++++++++++ README.md | 39 ++++- exemptions/allow.json | 10 ++ exemptions/templates/allowlist.rego.tpl | 34 ++++ exemptions/update_policy.py | 104 ++++++++++++ 6 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/apply-exemptions.yml create mode 100644 .gitignore create mode 100644 exemptions/allow.json create mode 100644 exemptions/templates/allowlist.rego.tpl create mode 100644 exemptions/update_policy.py diff --git a/.github/workflows/apply-exemptions.yml b/.github/workflows/apply-exemptions.yml new file mode 100644 index 0000000..ac2be07 --- /dev/null +++ b/.github/workflows/apply-exemptions.yml @@ -0,0 +1,39 @@ +name: Apply EPM Exemptions + +on: + push: + branches: [main] + paths: + - "exemptions/allow.json" + +permissions: + contents: read + id-token: write + +jobs: + apply: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Cloudsmith OIDC login + uses: cloudsmith-io/cloudsmith-cli-action@v1.0.2 + with: + oidc-namespace: ${{ vars.CLOUDSMITH_WORKSPACE }} + oidc-service-slug: ${{ vars.CLOUDSMITH_SERVICE }} + oidc-auth-only: "true" + + - name: Install dependencies + run: pip install requests + + - name: Apply exemptions + env: + CLOUDSMITH_WORKSPACE: ${{ vars.CLOUDSMITH_WORKSPACE }} + ALLOW_POLICY_SLUG: ${{ secrets.ALLOW_POLICY_SLUG }} + CLOUDSMITH_TOKEN: ${{ env.CLOUDSMITH_API_KEY }} + run: python exemptions/update_policy.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd09a7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,208 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +exemptions/*.local.json + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ diff --git a/README.md b/README.md index 4c5bfb1..1c528f2 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,14 @@ These policies are intended to be readable, predictable, and suitable for enterp baseline/ advanced/ legacy/ +exemptions/ + allow.json + update_policy.py + templates/ + allowlist.rego.tpl +.github/workflows/ + opa-lint.yml + apply-exemptions.yml ``` ### baseline/ @@ -83,7 +91,7 @@ All policies in this repository are designed to be non-terminal and composable. A recommended precedence pattern for baseline deployments is: 1. Package age restore (make eligible packages available again) -2. Packge age quarantine (time-based quarantine) +2. Package age quarantine (time-based quarantine) 3. License policy (tagging or governance) 4. High-risk vulnerability policy (quarantine based on thresholds) 5. Exact allowlist exemption (explicit override) @@ -98,6 +106,34 @@ https://docs.cloudsmith.com/supply-chain-security/epm --- +## Managing Exemptions (GitOps Workflow) + +The allowlist policy in `baseline/` supports a GitOps-based exemption workflow. +Rather than editing policies manually, exemptions are stored in Git, reviewed via +Pull Requests, and automatically applied to Cloudsmith on merge. + +### How it works + +1. Maintain an exemption list in the format `format:name:version`: + +```json +[ + "python:requests:2.6.4", + "npm:left-pad:1.3.0" +] +``` + +2. Open a Pull Request for security/DevOps review. +3. On merge, a CI step regenerates the allowlist Rego policy from the exemption list and uploads it to Cloudsmith via the API. + +### Why this approach + +EPM policies embed exemption data directly in Rego. Managing exemptions via Git provides auditability, an approval gate, rollback capability, and a scalable alternative to manual policy edits. + +The allowlist exemption policy should be placed at a higher precedence than the vulnerability policy (position 5 in the recommended ordering above) so that explicitly approved packages bypass security enforcement. + +--- + ## Deployment Policies can be deployed using the Cloudsmith API or CLI. @@ -114,3 +150,4 @@ This repository is the single source of truth for: - Documentation examples - Secure baseline recommendations - Enterprise EPM enablement guidance + diff --git a/exemptions/allow.json b/exemptions/allow.json new file mode 100644 index 0000000..e6f76b4 --- /dev/null +++ b/exemptions/allow.json @@ -0,0 +1,10 @@ +[ + "python:requests:2.6.4", + "python:requests:2.6.1", + "npm:left-pad:1.3.0", + "python:botocore:1.42.53", + "maven:commons-compress:1.18", + "maven:jackson-core:2.19.1", + "maven:commons-io:2.11.0", + "maven:commons-io:2.6", + "maven:commons-io:2.13.0"] diff --git a/exemptions/templates/allowlist.rego.tpl b/exemptions/templates/allowlist.rego.tpl new file mode 100644 index 0000000..569f26f --- /dev/null +++ b/exemptions/templates/allowlist.rego.tpl @@ -0,0 +1,34 @@ +package cloudsmith + +default match := false + +############################################################ +# GENERATED FILE — DO NOT EDIT MANUALLY +# Managed by exemption workflow +############################################################ + +allowlist := { +{{ENTRIES}} +} + +pkg := input.v0.package + +pkg_key := sprintf( + "%s:%s:%s", + [pkg.format, pkg.name, pkg.version], +) + +match if { + pkg.format != null + pkg.name != null + pkg.version != null + pkg_key in allowlist +} + +reason[msg] if { + match + msg := sprintf( + "Explicit exemption approved: %s", + [pkg_key], + ) +} diff --git a/exemptions/update_policy.py b/exemptions/update_policy.py new file mode 100644 index 0000000..5a90caf --- /dev/null +++ b/exemptions/update_policy.py @@ -0,0 +1,104 @@ +import os +import json +import requests +from pathlib import Path + +# -------------------------------------------------- +# ENV CONFIG (GHA or local) +# -------------------------------------------------- + +WORKSPACE = os.environ["CLOUDSMITH_WORKSPACE"] +ALLOW_POLICY_SLUG = os.environ["ALLOW_POLICY_SLUG"] +API_TOKEN = os.environ["CLOUDSMITH_TOKEN"] + +BASE_DIR = Path(__file__).parent +ALLOW_FILE = BASE_DIR / "allow.json" +TEMPLATE_FILE = BASE_DIR / "templates" / "allowlist.rego.tpl" + +POLICY_URL = ( + f"https://api.cloudsmith.io/v2/workspaces/" + f"{WORKSPACE}/policies/{ALLOW_POLICY_SLUG}/" +) + +HEADERS = { + "Authorization": f"Bearer {API_TOKEN}", + "Content-Type": "application/json", +} + + +# -------------------------------------------------- +# LOAD DATA +# -------------------------------------------------- + +def load_allowlist(): + if not ALLOW_FILE.exists(): + raise Exception("allow.json missing") + + data = json.loads(ALLOW_FILE.read_text()) + + if not isinstance(data, list): + raise Exception("allow.json must contain a list") + + return sorted(set(data)) + + +# -------------------------------------------------- +# TEMPLATE RENDER +# -------------------------------------------------- + +def render_rego(entries): + + template = TEMPLATE_FILE.read_text() + + formatted = ",\n ".join(f'"{e}"' for e in entries) + + return template.replace("{{ENTRIES}}", formatted) + + +# -------------------------------------------------- +# CLOUDSMITH API +# -------------------------------------------------- + +def fetch_policy(): + r = requests.get(POLICY_URL, headers=HEADERS) + r.raise_for_status() + return r.json() + + +def update_policy(policy, rego): + + payload = { + "name": policy["name"], + "description": policy.get("description"), + "rego": rego, + "enabled": policy["enabled"], + "precedence": policy["precedence"], + "is_terminal": policy["is_terminal"], + } + + r = requests.put(POLICY_URL, headers=HEADERS, json=payload) + r.raise_for_status() + + +# -------------------------------------------------- +# MAIN +# -------------------------------------------------- + +def main(): + + entries = load_allowlist() + + if not entries: + raise Exception("Refusing to upload empty exemption list") + + rego = render_rego(entries) + + policy = fetch_policy() + + update_policy(policy, rego) + + print(f"✅ Applied {len(entries)} exemptions") + + +if __name__ == "__main__": + main() From 9094bee38c5e5f7d03bf57c521189b35f07d3a19 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:01:34 +0000 Subject: [PATCH 19/35] Change branch for exemption application workflow --- .github/workflows/apply-exemptions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/apply-exemptions.yml b/.github/workflows/apply-exemptions.yml index ac2be07..ec06337 100644 --- a/.github/workflows/apply-exemptions.yml +++ b/.github/workflows/apply-exemptions.yml @@ -2,7 +2,7 @@ name: Apply EPM Exemptions on: push: - branches: [main] + branches: [epm-baseline-refactor] paths: - "exemptions/allow.json" From 1b326116ab16c6d9d47961467a352325b8d19c6b Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:02:38 +0000 Subject: [PATCH 20/35] Add python:time:3.4.5 to allow.json --- exemptions/allow.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exemptions/allow.json b/exemptions/allow.json index e6f76b4..d2b1bb3 100644 --- a/exemptions/allow.json +++ b/exemptions/allow.json @@ -7,4 +7,5 @@ "maven:jackson-core:2.19.1", "maven:commons-io:2.11.0", "maven:commons-io:2.6", - "maven:commons-io:2.13.0"] + "maven:commons-io:2.13.0", + "python:time:3.4.5"] From 783e2f6d2632a53a13381bbe5dd6574818ec594b Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:10:50 +0000 Subject: [PATCH 21/35] Update high-risk-vulnerability.rego --- baseline/high-risk-vulnerability.rego | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index 34d6363..72bc311 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -1,5 +1,6 @@ -# title: High Risk Vulnerability -# description: Blocks packages with a CVSS score >= X where a fix is available. +# METADATA +# title: High-risk vulnerability block +# description: Block packages with high-severity vulnerabilities (CVSS >= 7.0) when fixes are available package cloudsmith default match := false From 65fcd3c2149a73c184949a24ae91bdef3e487cf0 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:56:31 +0000 Subject: [PATCH 22/35] Document GitOps workflow for policy exemptions Added section on GitOps workflow for managing policy exemptions. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 1c528f2..87c46df 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,16 @@ They are preserved for documentation history and migration reference. --- +### exemptions/ + +A GitOps workflow for managing policy exemptions. + +Rather than editing policies manually, exemptions are stored in `allow.json`, reviewed via Pull Requests, and automatically applied to Cloudsmith on merge via GitHub Actions. + +See the [Managing Exemptions](#managing-exemptions-gitops-workflow) section for details. + +--- + ## Policy Ordering & Precedence Cloudsmith EPM evaluates policies in precedence order (lowest precedence runs first). From 2d9911af5531ade77db8aea2f7bd39f052a617cf Mon Sep 17 00:00:00 2001 From: markmcmurray Date: Tue, 10 Mar 2026 17:14:14 +0000 Subject: [PATCH 23/35] add regal step to lint rego --- .github/workflows/opa-lint.yml | 9 +++++++++ .regal/config.yaml | 12 ++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 .regal/config.yaml diff --git a/.github/workflows/opa-lint.yml b/.github/workflows/opa-lint.yml index 724ba7f..e4bbdc9 100644 --- a/.github/workflows/opa-lint.yml +++ b/.github/workflows/opa-lint.yml @@ -34,6 +34,15 @@ jobs: echo "No advanced folder" fi + - name: Install Regal + run: | + curl -L -o regal https://github.com/StyraInc/regal/releases/latest/download/regal_Linux_x86_64 + chmod +x regal + sudo mv regal /usr/local/bin/regal + + - name: Lint with Regal + run: regal lint baseline advanced + - name: Validate Policies run: | find baseline advanced -name "*.rego" -print0 | \ diff --git a/.regal/config.yaml b/.regal/config.yaml new file mode 100644 index 0000000..53c769a --- /dev/null +++ b/.regal/config.yaml @@ -0,0 +1,12 @@ +rules: + idiomatic: + directory-package-mismatch: + # All policies use `package cloudsmith` as required by the Cloudsmith EPM runtime + level: ignore + no-defined-entrypoint: + # Entrypoints are determined by the EPM system, not metadata annotations + level: ignore + style: + messy-rule: + # `default match := false` + `match if { ... }` is the required EPM policy interface + level: ignore From ced740b91241690b546ee057799139f074bcd3da Mon Sep 17 00:00:00 2001 From: markmcmurray Date: Tue, 10 Mar 2026 17:30:26 +0000 Subject: [PATCH 24/35] tidy rego based on regal output --- advanced/huggingface-recipes/model_card.rego | 2 +- advanced/huggingface-recipes/risky_files.rego | 9 +++++++-- advanced/huggingface-recipes/security_scan.rego | 2 +- advanced/huggingface-recipes/trusted_publishers.rego | 2 +- baseline/high-risk-vulnerability.rego | 4 ++-- baseline/malware-block.rego | 2 ++ 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/advanced/huggingface-recipes/model_card.rego b/advanced/huggingface-recipes/model_card.rego index c34c280..ad540cd 100644 --- a/advanced/huggingface-recipes/model_card.rego +++ b/advanced/huggingface-recipes/model_card.rego @@ -4,7 +4,7 @@ default match := false pkg := input.v0.package -hf_pkg if "huggingface" == pkg.format +hf_pkg if pkg.format == "huggingface" match if { hf_pkg diff --git a/advanced/huggingface-recipes/risky_files.rego b/advanced/huggingface-recipes/risky_files.rego index b06a02c..2e81dc1 100644 --- a/advanced/huggingface-recipes/risky_files.rego +++ b/advanced/huggingface-recipes/risky_files.rego @@ -4,7 +4,7 @@ default match := false pkg := input.v0.package -hf_pkg if "huggingface" == pkg.format +hf_pkg if pkg.format == "huggingface" # Upstream packages are fetched by a system user is_upstream_pkg if input.v0.package.uploader.slug == "cloudsmith-o6v" @@ -20,7 +20,12 @@ is_upstream_pkg if input.v0.package.uploader.slug == "cloudsmith-o6v" # SavedModel (.pb) # GGUF (.gguf) -risky_file_extensions := {".h5", ".hdf5", ".pdparams", ".keras", ".bin", ".pkl", ".dat", ".pt", ".pth", ".ckpt", ".npy", ".joblib", ".dill", ".pb", ".gguf", ".zip"} +risky_file_extensions := { + ".bin", ".ckpt", ".dat", ".dill", + ".gguf", ".h5", ".hdf5", ".joblib", + ".keras", ".npy", ".pb", ".pdparams", + ".pkl", ".pt", ".pth", ".zip", +} match if { hf_pkg diff --git a/advanced/huggingface-recipes/security_scan.rego b/advanced/huggingface-recipes/security_scan.rego index bfb8740..8c4d5b3 100644 --- a/advanced/huggingface-recipes/security_scan.rego +++ b/advanced/huggingface-recipes/security_scan.rego @@ -17,7 +17,7 @@ incomplete_or_unsafe if { } match if { - "huggingface" == input.v0.package.format + input.v0.package.format == "huggingface" is_upstream_pkg incomplete_or_unsafe } diff --git a/advanced/huggingface-recipes/trusted_publishers.rego b/advanced/huggingface-recipes/trusted_publishers.rego index 767c6ef..3570ab7 100644 --- a/advanced/huggingface-recipes/trusted_publishers.rego +++ b/advanced/huggingface-recipes/trusted_publishers.rego @@ -10,7 +10,7 @@ verified_publishers := {"amazon", "apple", "facebook", "FacebookAI", "google", " publisher := split(input.v0.package.name, "/")[0] match if { - "huggingface" == input.v0.package.format + input.v0.package.format == "huggingface" is_upstream_pkg publisher in verified_publishers } diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index 72bc311..eed26b5 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -3,6 +3,8 @@ # description: Block packages with high-severity vulnerabilities (CVSS >= 7.0) when fixes are available package cloudsmith +import rego.v1 + default match := false min_cvss := 7.0 @@ -32,13 +34,11 @@ cvss_scores_for_record(v) := top_level | affected_level if { top_level := {s | some sev in v.severity sev.numerical_score != null - s := sev.numerical_score } affected_level := {s | some a in v.affected some sev in a.severity sev.numerical_score != null - s := sev.numerical_score } } diff --git a/baseline/malware-block.rego b/baseline/malware-block.rego index 94a8d00..7abaddb 100644 --- a/baseline/malware-block.rego +++ b/baseline/malware-block.rego @@ -1,5 +1,7 @@ package cloudsmith +import rego.v1 + default match := false match if { From 10f6b28153b5c6d594da8d893247a10767f60701 Mon Sep 17 00:00:00 2001 From: markmcmurray Date: Wed, 11 Mar 2026 10:26:58 +0000 Subject: [PATCH 25/35] relint high-risk-vulnerability --- baseline/high-risk-vulnerability.rego | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index eed26b5..a90c45a 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -23,7 +23,7 @@ aliased_records[k] := records if { records := {v | some v in input.v0.osv; k in vuln_keys(v)} } -blocking_keys := {k | +blocking_keys contains k if { some k, _ in aliased_records not k in ignored max_cvss_for_key(k) >= min_cvss From 275f10a4f16d8a8de38d966cf8dc9846eac0f014 Mon Sep 17 00:00:00 2001 From: markmcmurray Date: Wed, 11 Mar 2026 10:34:49 +0000 Subject: [PATCH 26/35] fix unsafe var introduced by linting fix --- baseline/high-risk-vulnerability.rego | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index a90c45a..d388d77 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -31,11 +31,11 @@ blocking_keys contains k if { } cvss_scores_for_record(v) := top_level | affected_level if { - top_level := {s | + top_level := {sev.numerical_score | some sev in v.severity sev.numerical_score != null } - affected_level := {s | + affected_level := {sev.numerical_score | some a in v.affected some sev in a.severity sev.numerical_score != null From d98b3453f362dc1b4b1807ec674f4a30b488953e Mon Sep 17 00:00:00 2001 From: Mark McMurray Date: Wed, 11 Mar 2026 11:06:01 +0000 Subject: [PATCH 27/35] remove leftover rego.v1 imports Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- baseline/high-risk-vulnerability.rego | 2 -- baseline/malware-block.rego | 2 -- 2 files changed, 4 deletions(-) diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index d388d77..6c0fc5d 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -3,8 +3,6 @@ # description: Block packages with high-severity vulnerabilities (CVSS >= 7.0) when fixes are available package cloudsmith -import rego.v1 - default match := false min_cvss := 7.0 diff --git a/baseline/malware-block.rego b/baseline/malware-block.rego index 7abaddb..94a8d00 100644 --- a/baseline/malware-block.rego +++ b/baseline/malware-block.rego @@ -1,7 +1,5 @@ package cloudsmith -import rego.v1 - default match := false match if { From 804341be1404a58584622ce51dad3c07ca89bef0 Mon Sep 17 00:00:00 2001 From: markmcmurray Date: Tue, 10 Mar 2026 20:10:12 +0000 Subject: [PATCH 28/35] add unit tests for rego --- .github/workflows/opa-lint.yml | 10 ++ .../huggingface-recipes/model_card_test.rego | 36 ++++++ .../huggingface-recipes/risky_files_test.rego | 50 +++++++++ .../security_scan_test.rego | 45 ++++++++ .../trusted_publishers_test.rego | 39 +++++++ baseline/exact-allowlist-exemption_test.rego | 41 +++++++ baseline/exact-blocklist_test.rego | 41 +++++++ baseline/high-risk-vulnerability.rego | 27 ++++- baseline/high-risk-vulnerability_test.rego | 103 ++++++++++++++++++ baseline/license-compliance_test.rego | 36 ++++++ baseline/malware-block_test.rego | 29 +++++ baseline/package-age-quarantine_test.rego | 26 +++++ baseline/package-age-restore_test.rego | 26 +++++ 13 files changed, 505 insertions(+), 4 deletions(-) create mode 100644 advanced/huggingface-recipes/model_card_test.rego create mode 100644 advanced/huggingface-recipes/risky_files_test.rego create mode 100644 advanced/huggingface-recipes/security_scan_test.rego create mode 100644 advanced/huggingface-recipes/trusted_publishers_test.rego create mode 100644 baseline/exact-allowlist-exemption_test.rego create mode 100644 baseline/exact-blocklist_test.rego create mode 100644 baseline/high-risk-vulnerability_test.rego create mode 100644 baseline/license-compliance_test.rego create mode 100644 baseline/malware-block_test.rego create mode 100644 baseline/package-age-quarantine_test.rego create mode 100644 baseline/package-age-restore_test.rego diff --git a/.github/workflows/opa-lint.yml b/.github/workflows/opa-lint.yml index e4bbdc9..a48a2d3 100644 --- a/.github/workflows/opa-lint.yml +++ b/.github/workflows/opa-lint.yml @@ -50,3 +50,13 @@ jobs: echo "Checking $file" opa check "$file" done + + - name: Unit Test Policies + run: | + find baseline advanced -name "*_test.rego" -print0 | \ + while IFS= read -r -d '' test_file; do + policy="${test_file/_test/}" + echo "Testing $test_file" + opa test "$policy" "$test_file" + done + diff --git a/advanced/huggingface-recipes/model_card_test.rego b/advanced/huggingface-recipes/model_card_test.rego new file mode 100644 index 0000000..e738108 --- /dev/null +++ b/advanced/huggingface-recipes/model_card_test.rego @@ -0,0 +1,36 @@ +package cloudsmith_test + +test_match_target_dataset if { + data.cloudsmith.match with input as {"v0": {"package": { + "format": "huggingface", + "card": {"datasets": ["HuggingFaceTB/smollm-corpus"]}, + }}} +} + +test_match_dataset_among_multiple if { + data.cloudsmith.match with input as {"v0": {"package": { + "format": "huggingface", + "card": {"datasets": ["other/dataset", "HuggingFaceTB/smollm-corpus"]}, + }}} +} + +test_no_match_wrong_format if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "python", + "card": {"datasets": ["HuggingFaceTB/smollm-corpus"]}, + }}} +} + +test_no_match_different_dataset if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "huggingface", + "card": {"datasets": ["some/other-dataset"]}, + }}} +} + +test_no_match_empty_datasets if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "huggingface", + "card": {"datasets": []}, + }}} +} diff --git a/advanced/huggingface-recipes/risky_files_test.rego b/advanced/huggingface-recipes/risky_files_test.rego new file mode 100644 index 0000000..c0e2b2c --- /dev/null +++ b/advanced/huggingface-recipes/risky_files_test.rego @@ -0,0 +1,50 @@ +package cloudsmith_test + +_upstream_hf_pkg(files) := {"v0": {"package": { + "format": "huggingface", + "uploader": {"slug": "cloudsmith-o6v"}, + "files": files, +}}} + +test_match_bin_file if { + data.cloudsmith.match with input as _upstream_hf_pkg([{"file_extension": ".bin"}]) +} + +test_match_pkl_file if { + data.cloudsmith.match with input as _upstream_hf_pkg([{"file_extension": ".pkl"}]) +} + +test_match_gguf_file if { + data.cloudsmith.match with input as _upstream_hf_pkg([{"file_extension": ".gguf"}]) +} + +test_match_risky_among_safe_files if { + data.cloudsmith.match with input as _upstream_hf_pkg([ + {"file_extension": ".txt"}, + {"file_extension": ".bin"}, + ]) +} + +test_no_match_safe_file_extension if { + not data.cloudsmith.match with input as _upstream_hf_pkg([{"file_extension": ".txt"}]) +} + +test_no_match_not_upstream if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "huggingface", + "uploader": {"slug": "other-user"}, + "files": [{"file_extension": ".bin"}], + }}} +} + +test_no_match_wrong_format if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "python", + "uploader": {"slug": "cloudsmith-o6v"}, + "files": [{"file_extension": ".bin"}], + }}} +} + +test_no_match_empty_files if { + not data.cloudsmith.match with input as _upstream_hf_pkg([]) +} diff --git a/advanced/huggingface-recipes/security_scan_test.rego b/advanced/huggingface-recipes/security_scan_test.rego new file mode 100644 index 0000000..6cf9191 --- /dev/null +++ b/advanced/huggingface-recipes/security_scan_test.rego @@ -0,0 +1,45 @@ +package cloudsmith_test + +_upstream_hf := {"format": "huggingface", "uploader": {"slug": "cloudsmith-o6v"}} + +test_match_incomplete_scan if { + data.cloudsmith.match with input as {"v0": { + "package": _upstream_hf, + "model_security": {"availability": "INCOMPLETE", "scan_summary": "SAFE"}, + }} +} + +test_match_unsafe_scan if { + data.cloudsmith.match with input as {"v0": { + "package": _upstream_hf, + "model_security": {"availability": "COMPLETE", "scan_summary": "UNSAFE"}, + }} +} + +test_match_incomplete_and_unsafe if { + data.cloudsmith.match with input as {"v0": { + "package": _upstream_hf, + "model_security": {"availability": "INCOMPLETE", "scan_summary": "UNSAFE"}, + }} +} + +test_no_match_complete_and_safe if { + not data.cloudsmith.match with input as {"v0": { + "package": _upstream_hf, + "model_security": {"availability": "COMPLETE", "scan_summary": "SAFE"}, + }} +} + +test_no_match_not_upstream if { + not data.cloudsmith.match with input as {"v0": { + "package": {"format": "huggingface", "uploader": {"slug": "other-user"}}, + "model_security": {"availability": "INCOMPLETE", "scan_summary": "SAFE"}, + }} +} + +test_no_match_wrong_format if { + not data.cloudsmith.match with input as {"v0": { + "package": {"format": "python", "uploader": {"slug": "cloudsmith-o6v"}}, + "model_security": {"availability": "INCOMPLETE", "scan_summary": "SAFE"}, + }} +} diff --git a/advanced/huggingface-recipes/trusted_publishers_test.rego b/advanced/huggingface-recipes/trusted_publishers_test.rego new file mode 100644 index 0000000..2ff67fc --- /dev/null +++ b/advanced/huggingface-recipes/trusted_publishers_test.rego @@ -0,0 +1,39 @@ +package cloudsmith_test + +_upstream_hf_package(name) := {"v0": {"package": { + "format": "huggingface", + "uploader": {"slug": "cloudsmith-o6v"}, + "name": name, +}}} + +test_match_google_model if { + data.cloudsmith.match with input as _upstream_hf_package("google/gemma-2") +} + +test_match_microsoft_model if { + data.cloudsmith.match with input as _upstream_hf_package("microsoft/phi-3") +} + +test_match_openai_model if { + data.cloudsmith.match with input as _upstream_hf_package("openai/whisper-large") +} + +test_no_match_unknown_publisher if { + not data.cloudsmith.match with input as _upstream_hf_package("unknown-org/some-model") +} + +test_no_match_not_upstream if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "huggingface", + "uploader": {"slug": "other-user"}, + "name": "google/gemma-2", + }}} +} + +test_no_match_wrong_format if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "python", + "uploader": {"slug": "cloudsmith-o6v"}, + "name": "google/some-package", + }}} +} diff --git a/baseline/exact-allowlist-exemption_test.rego b/baseline/exact-allowlist-exemption_test.rego new file mode 100644 index 0000000..21e0580 --- /dev/null +++ b/baseline/exact-allowlist-exemption_test.rego @@ -0,0 +1,41 @@ +package cloudsmith_test + +test_match_allowlisted_python_package if { + data.cloudsmith.match with input as {"v0": {"package": { + "format": "python", + "name": "example-lib", + "version": "1.2.3", + }}} +} + +test_match_allowlisted_npm_package if { + data.cloudsmith.match with input as {"v0": {"package": { + "format": "npm", + "name": "example-ui", + "version": "4.5.6", + }}} +} + +test_no_match_wrong_version if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "python", + "name": "example-lib", + "version": "9.9.9", + }}} +} + +test_no_match_wrong_format if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "ruby", + "name": "example-lib", + "version": "1.2.3", + }}} +} + +test_reason_message if { + data.cloudsmith.reason["Explicit exemption approved: python:example-lib:1.2.3"] with input as {"v0": {"package": { + "format": "python", + "name": "example-lib", + "version": "1.2.3", + }}} +} diff --git a/baseline/exact-blocklist_test.rego b/baseline/exact-blocklist_test.rego new file mode 100644 index 0000000..07ec90d --- /dev/null +++ b/baseline/exact-blocklist_test.rego @@ -0,0 +1,41 @@ +package cloudsmith_test + +test_match_blocked_python_package if { + data.cloudsmith.match with input as {"v0": {"package": { + "format": "python", + "name": "malicious-lib", + "version": "0.1.0", + }}} +} + +test_match_blocked_npm_package if { + data.cloudsmith.match with input as {"v0": {"package": { + "format": "npm", + "name": "compromised-ui", + "version": "9.9.9", + }}} +} + +test_no_match_unlisted_package if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "python", + "name": "safe-lib", + "version": "1.0.0", + }}} +} + +test_no_match_wrong_version if { + not data.cloudsmith.match with input as {"v0": {"package": { + "format": "python", + "name": "malicious-lib", + "version": "9.9.9", + }}} +} + +test_reason_message if { + data.cloudsmith.reason["Blocked by explicit deny list: python:malicious-lib:0.1.0"] with input as {"v0": {"package": { + "format": "python", + "name": "malicious-lib", + "version": "0.1.0", + }}} +} diff --git a/baseline/high-risk-vulnerability.rego b/baseline/high-risk-vulnerability.rego index 6c0fc5d..22524a8 100644 --- a/baseline/high-risk-vulnerability.rego +++ b/baseline/high-risk-vulnerability.rego @@ -48,16 +48,35 @@ max_cvss_for_key(k) := max(scores) if { count(scores) > 0 } else := 0.0 -has_fix_for_key(k) if { - some v in aliased_records[k] +fixed_versions_for_key(k) := {e.fixed | + some v in input.v0.osv + k in vuln_keys(v) some a in v.affected some r in a.ranges some e in r.events - e.fixed + include_fix(e.fixed) +} + +include_fix(f) if { + semver.is_valid(f) + semver.is_valid(input.v0.package.version) + semver.compare(f, input.v0.package.version) > 0 +} + +include_fix(f) if not semver.is_valid(f) + +include_fix(f) if { + semver.is_valid(f) + not semver.is_valid(input.v0.package.version) +} + +has_fix_for_key(k) if { + count(fixed_versions_for_key(k)) > 0 } reason contains msg if { some k in blocking_keys score := max_cvss_for_key(k) - msg := sprintf("Blocking %v: CVSS %.1f, fix available", [k, score]) + fixes := fixed_versions_for_key(k) + msg := sprintf("Blocking %v: CVSS %.1f, fix(es) available: %v", [k, score, concat(", ", fixes)]) } diff --git a/baseline/high-risk-vulnerability_test.rego b/baseline/high-risk-vulnerability_test.rego new file mode 100644 index 0000000..bce03bb --- /dev/null +++ b/baseline/high-risk-vulnerability_test.rego @@ -0,0 +1,103 @@ +package cloudsmith_test + +import rego.v1 + +# ── Shared fixtures ─────────────────────────────────────────────────────────── + +_package := {"version": "1.0.0", "format": "python", "name": "example"} + +_vuln_with_fix(id, score) := { + "id": id, + "aliases": [], + "severity": [{"numerical_score": score}], + "affected": [{"severity": [], "ranges": [{"events": [{"fixed": "1.0.1"}]}]}], +} + +_vuln_no_fix(id, score) := { + "id": id, + "aliases": [], + "severity": [{"numerical_score": score}], + "affected": [{"severity": [], "ranges": [{"events": []}]}], +} + +# ── Blocking ────────────────────────────────────────────────────────────────── + +test_match_high_cvss_with_fix if { + data.cloudsmith.match with input as {"v0": { + "osv": [_vuln_with_fix("CVE-2021-1234", 9.0)], + "package": _package, + }} +} + +test_match_at_cvss_threshold if { + data.cloudsmith.match with input as {"v0": { + "osv": [_vuln_with_fix("CVE-2021-1234", 7.0)], + "package": _package, + }} +} + +test_match_cvss_from_affected_severity if { + data.cloudsmith.match with input as {"v0": { + "osv": [{ + "id": "CVE-2021-1234", + "aliases": [], + "severity": [], + "affected": [{"severity": [{"numerical_score": 9.0}], "ranges": [{"events": [{"fixed": "1.0.1"}]}]}], + }], + "package": _package, + }} +} + +test_match_uses_alias_as_key if { + data.cloudsmith.match with input as {"v0": { + "osv": [{ + "id": "GHSA-xxxx-xxxx-xxxx", + "aliases": ["CVE-2021-9999"], + "severity": [{"numerical_score": 9.0}], + "affected": [{"severity": [], "ranges": [{"events": [{"fixed": "1.0.1"}]}]}], + }], + "package": _package, + }} +} + +# ── Not blocking ────────────────────────────────────────────────────────────── + +test_no_match_below_cvss_threshold if { + not data.cloudsmith.match with input as {"v0": { + "osv": [_vuln_with_fix("CVE-2021-5678", 6.9)], + "package": _package, + }} +} + +test_no_match_high_cvss_no_fix if { + not data.cloudsmith.match with input as {"v0": { + "osv": [_vuln_no_fix("CVE-2021-9999", 9.0)], + "package": _package, + }} +} + +test_no_match_fix_already_applied if { + not data.cloudsmith.match with input as {"v0": { + "osv": [{ + "id": "CVE-2021-0001", + "aliases": [], + "severity": [{"numerical_score": 9.0}], + "affected": [{"severity": [], "ranges": [{"events": [{"fixed": "0.9.0"}]}]}], + }], + "package": _package, + }} +} + +test_no_match_empty_osv if { + not data.cloudsmith.match with input as {"v0": {"osv": [], "package": _package}} +} + +# ── Reason messages ─────────────────────────────────────────────────────────── + +test_reason_message if { + r := data.cloudsmith.reason with input as {"v0": { + "osv": [_vuln_with_fix("CVE-2021-1234", 9.0)], + "package": _package, + }} + r["Blocking CVE-2021-1234: CVSS 9.0, fix(es) available: 1.0.1"] +} diff --git a/baseline/license-compliance_test.rego b/baseline/license-compliance_test.rego new file mode 100644 index 0000000..889e1b5 --- /dev/null +++ b/baseline/license-compliance_test.rego @@ -0,0 +1,36 @@ +package cloudsmith_test + +_input(spdx) := {"v0": {"package": {"license": {"oss_license": {"spdx_identifier": spdx}}}}} + +test_match_gpl3 if { + data.cloudsmith.match with input as _input("GPL-3.0-only") +} + +test_match_agpl if { + data.cloudsmith.match with input as _input("AGPL-3.0-only") +} + +test_match_lgpl if { + data.cloudsmith.match with input as _input("LGPL-2.1-only") +} + +test_no_match_mit if { + not data.cloudsmith.match with input as _input("MIT") +} + +test_no_match_apache if { + not data.cloudsmith.match with input as _input("Apache-2.0") +} + +test_no_match_null_license if { + not data.cloudsmith.match with input as {"v0": {"package": {"license": null}}} +} + +test_no_match_null_oss_license if { + not data.cloudsmith.match with input as {"v0": {"package": {"license": {"oss_license": null}}}} +} + +test_reason_message if { + expected := "Copyleft license detected (GPL-3.0-only). Package blocked/quarantined per license policy." + data.cloudsmith.reason[expected] with input as _input("GPL-3.0-only") +} diff --git a/baseline/malware-block_test.rego b/baseline/malware-block_test.rego new file mode 100644 index 0000000..5715bea --- /dev/null +++ b/baseline/malware-block_test.rego @@ -0,0 +1,29 @@ +package cloudsmith_test + +test_match_malicious_osv_id if { + data.cloudsmith.match with input as {"v0": {"osv": [{"id": "MAL-1234"}]}} +} + +test_match_multiple_osv_one_malicious if { + data.cloudsmith.match with input as {"v0": {"osv": [ + {"id": "CVE-2021-0001"}, + {"id": "MAL-5678"}, + ]}} +} + +test_no_match_non_malicious_osv if { + not data.cloudsmith.match with input as {"v0": {"osv": [{"id": "CVE-2021-1234"}]}} +} + +test_no_match_empty_osv if { + not data.cloudsmith.match with input as {"v0": {"osv": []}} +} + +test_no_match_null_osv if { + not data.cloudsmith.match with input as {"v0": {"osv": null}} +} + +test_reason_message if { + r := data.cloudsmith.reason with input as {"v0": {"osv": [{"id": "MAL-1234"}]}} + count(r) > 0 +} diff --git a/baseline/package-age-quarantine_test.rego b/baseline/package-age-quarantine_test.rego new file mode 100644 index 0000000..4e270ff --- /dev/null +++ b/baseline/package-age-quarantine_test.rego @@ -0,0 +1,26 @@ +package cloudsmith_test + +_input(published_at) := {"v0": {"package": {"upstream_metadata": {"published_at": published_at}}}} + +# "2099-01-01" is always in the future, so always within the past 3 days relative to now +test_match_recently_published if { + data.cloudsmith.match with input as _input("2099-01-01T00:00:00Z") +} + +# "2020-01-01" is well in the past, so always older than 3 days +test_no_match_old_package if { + not data.cloudsmith.match with input as _input("2020-01-01T00:00:00Z") +} + +test_no_match_null_upstream_metadata if { + not data.cloudsmith.match with input as {"v0": {"package": {"upstream_metadata": null}}} +} + +test_no_match_null_published_at if { + not data.cloudsmith.match with input as {"v0": {"package": {"upstream_metadata": {"published_at": null}}}} +} + +test_reason_message if { + r := data.cloudsmith.reason with input as _input("2099-01-01T00:00:00Z") + count(r) > 0 +} diff --git a/baseline/package-age-restore_test.rego b/baseline/package-age-restore_test.rego new file mode 100644 index 0000000..d37381b --- /dev/null +++ b/baseline/package-age-restore_test.rego @@ -0,0 +1,26 @@ +package cloudsmith_test + +_input(published_at) := {"v0": {"package": {"upstream_metadata": {"published_at": published_at}}}} + +# "2020-01-01" is well in the past, so always older than 3 days +test_match_old_package if { + data.cloudsmith.match with input as _input("2020-01-01T00:00:00Z") +} + +# "2099-01-01" is always in the future, so always within the past 3 days relative to now +test_no_match_recently_published if { + not data.cloudsmith.match with input as _input("2099-01-01T00:00:00Z") +} + +test_no_match_null_upstream_metadata if { + not data.cloudsmith.match with input as {"v0": {"package": {"upstream_metadata": null}}} +} + +test_no_match_null_published_at if { + not data.cloudsmith.match with input as {"v0": {"package": {"upstream_metadata": {"published_at": null}}}} +} + +test_reason_message if { + r := data.cloudsmith.reason with input as _input("2020-01-01T00:00:00Z") + count(r) > 0 +} From d934db41d0235effa76a208527d606e48d1d105b Mon Sep 17 00:00:00 2001 From: Mark McMurray Date: Wed, 11 Mar 2026 11:20:39 +0000 Subject: [PATCH 29/35] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- baseline/high-risk-vulnerability_test.rego | 2 -- 1 file changed, 2 deletions(-) diff --git a/baseline/high-risk-vulnerability_test.rego b/baseline/high-risk-vulnerability_test.rego index bce03bb..f1e89c8 100644 --- a/baseline/high-risk-vulnerability_test.rego +++ b/baseline/high-risk-vulnerability_test.rego @@ -1,7 +1,5 @@ package cloudsmith_test -import rego.v1 - # ── Shared fixtures ─────────────────────────────────────────────────────────── _package := {"version": "1.0.0", "format": "python", "name": "example"} From 3e6137e8d6aac2a1d7bf0bf25c2b1b48b26fc7c6 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:38:02 +0000 Subject: [PATCH 30/35] Update baseline/license-compliance.rego Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- baseline/license-compliance.rego | 40 ++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/baseline/license-compliance.rego b/baseline/license-compliance.rego index e629aa5..6a9a38b 100644 --- a/baseline/license-compliance.rego +++ b/baseline/license-compliance.rego @@ -3,16 +3,36 @@ package cloudsmith default match := false copyleft := { - "GPL-1.0-only", "GPL-1.0-or-later", "GPL-2.0", "GPL-2.0-only", - "GPL-2.0-or-later", "GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later", - "LGPL-2.0", "LGPL-2.0-only", "LGPL-2.0-or-later", - "LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later", - "LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later", - "AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later", - "MPL-1.0", "MPL-1.1", "MPL-2.0", - "CDDL-1.0", "CDDL-1.1", - "EPL-1.0", "EPL-2.0", - "OSL-1.0", "OSL-2.0", "OSL-3.0", + "GPL-1.0-only", + "GPL-1.0-or-later", + "GPL-2.0", + "GPL-2.0-only", + "GPL-2.0-or-later", + "GPL-3.0", + "GPL-3.0-only", + "GPL-3.0-or-later", + "LGPL-2.0", + "LGPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1", + "LGPL-2.1-only", + "LGPL-2.1-or-later", + "LGPL-3.0", + "LGPL-3.0-only", + "LGPL-3.0-or-later", + "AGPL-3.0", + "AGPL-3.0-only", + "AGPL-3.0-or-later", + "MPL-1.0", + "MPL-1.1", + "MPL-2.0", + "CDDL-1.0", + "CDDL-1.1", + "EPL-1.0", + "EPL-2.0", + "OSL-1.0", + "OSL-2.0", + "OSL-3.0", "SSPL-1.0", } From c85bc8a40723978a31ace1cf20ee6ac94dc4af57 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:38:36 +0000 Subject: [PATCH 31/35] Update exemptions/update_policy.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- exemptions/update_policy.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/exemptions/update_policy.py b/exemptions/update_policy.py index 5a90caf..48a7a38 100644 --- a/exemptions/update_policy.py +++ b/exemptions/update_policy.py @@ -46,12 +46,29 @@ def load_allowlist(): # TEMPLATE RENDER # -------------------------------------------------- +def _validate_allowlist_entry(entry): + """ + Validate that an allowlist entry is a string in 'format:name:version' form. + """ + if not isinstance(entry, str): + raise ValueError("Allowlist entries must be strings in 'format:name:version' form") + + # Require exactly two ':' separators to roughly enforce 'format:name:version'. + if entry.count(":") != 2: + raise ValueError(f"Invalid allowlist entry '{entry}'; expected 'format:name:version'") + + return entry + + def render_rego(entries): template = TEMPLATE_FILE.read_text() - formatted = ",\n ".join(f'"{e}"' for e in entries) + # Validate entries and ensure they are in the expected shape. + validated_entries = [_validate_allowlist_entry(e) for e in entries] + # Use JSON encoding to safely escape values for inclusion in Rego. + formatted = ",\n ".join(json.dumps(e) for e in validated_entries) return template.replace("{{ENTRIES}}", formatted) From 191d72bf90a7e38a274346f773d1f179ed7c96b4 Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:39:10 +0000 Subject: [PATCH 32/35] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87c46df..229fcc6 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ They are preserved for documentation history and migration reference. A GitOps workflow for managing policy exemptions. -Rather than editing policies manually, exemptions are stored in `allow.json`, reviewed via Pull Requests, and automatically applied to Cloudsmith on merge via GitHub Actions. +Rather than editing policies manually, exemptions are stored in `allow.json`, reviewed via Pull Requests, and automatically applied to Cloudsmith by GitHub Actions when changes are pushed to the `epm-baseline-refactor` branch. See the [Managing Exemptions](#managing-exemptions-gitops-workflow) section for details. From 94e8c0e912c8ca408a72a42929a28fa5c68dfa8e Mon Sep 17 00:00:00 2001 From: Ciara Carey <84123925+ciaracarey@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:40:09 +0000 Subject: [PATCH 33/35] Update .github/workflows/apply-exemptions.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/apply-exemptions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/apply-exemptions.yml b/.github/workflows/apply-exemptions.yml index ec06337..8996f2a 100644 --- a/.github/workflows/apply-exemptions.yml +++ b/.github/workflows/apply-exemptions.yml @@ -2,7 +2,7 @@ name: Apply EPM Exemptions on: push: - branches: [epm-baseline-refactor] + branches: [epm-baseline-refactor, main] paths: - "exemptions/allow.json" From 2cdfee80ad2d4a2a4eef6efa35e74ac27690d80f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:49:04 +0000 Subject: [PATCH 34/35] Initial plan From b4364900116e0df5c6597d6e0fec4581a96aa133 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:52:08 +0000 Subject: [PATCH 35/35] Add explicit timeout and retry/backoff to Cloudsmith API requests Co-authored-by: ciaracarey <84123925+ciaracarey@users.noreply.github.com> --- exemptions/update_policy.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/exemptions/update_policy.py b/exemptions/update_policy.py index 48a7a38..6a08213 100644 --- a/exemptions/update_policy.py +++ b/exemptions/update_policy.py @@ -2,6 +2,8 @@ import json import requests from pathlib import Path +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry # -------------------------------------------------- # ENV CONFIG (GHA or local) @@ -25,6 +27,30 @@ "Content-Type": "application/json", } +# Seconds to wait for a response from the Cloudsmith API before giving up. +REQUEST_TIMEOUT = 30 + +# Retry up to 3 times on connection errors and 5xx responses, with exponential +# backoff (0.5 s, 1 s, 2 s) so transient failures resolve quickly. +_retry_strategy = Retry( + total=3, + backoff_factor=0.5, + status_forcelist=[500, 502, 503, 504], + allowed_methods=["GET", "PUT"], +) + + +def _build_session() -> requests.Session: + session = requests.Session() + adapter = HTTPAdapter(max_retries=_retry_strategy) + # Only mount on HTTPS to prevent tokens being sent in cleartext. + session.mount("https://", adapter) + return session + + +# Single shared session so the retry adapter is not recreated per request. +_session = _build_session() + # -------------------------------------------------- # LOAD DATA @@ -77,7 +103,7 @@ def render_rego(entries): # -------------------------------------------------- def fetch_policy(): - r = requests.get(POLICY_URL, headers=HEADERS) + r = _session.get(POLICY_URL, headers=HEADERS, timeout=REQUEST_TIMEOUT) r.raise_for_status() return r.json() @@ -93,7 +119,7 @@ def update_policy(policy, rego): "is_terminal": policy["is_terminal"], } - r = requests.put(POLICY_URL, headers=HEADERS, json=payload) + r = _session.put(POLICY_URL, headers=HEADERS, json=payload, timeout=REQUEST_TIMEOUT) r.raise_for_status()