diff --git a/.github/workflows/apply-exemptions.yml b/.github/workflows/apply-exemptions.yml new file mode 100644 index 0000000..8996f2a --- /dev/null +++ b/.github/workflows/apply-exemptions.yml @@ -0,0 +1,39 @@ +name: Apply EPM Exemptions + +on: + push: + branches: [epm-baseline-refactor, 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/.github/workflows/opa-lint.yml b/.github/workflows/opa-lint.yml new file mode 100644 index 0000000..a48a2d3 --- /dev/null +++ b/.github/workflows/opa-lint.yml @@ -0,0 +1,62 @@ +name: OPA Lint & Validate + +on: + pull_request: + push: + branches: [main, epm-baseline-refactor] + +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: | + 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: 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 | \ + while IFS= read -r -d '' file; do + 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/.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/.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 diff --git a/README.md b/README.md index b28f95f..229fcc6 100644 --- a/README.md +++ b/README.md @@ -1,537 +1,163 @@ # 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" -``` +This repository contains curated, production-ready Open Policy Agent (OPA) policies for use with Cloudsmith Enterprise Policy Management (EPM). -Note: If you have a tagging response action attached to your policy, you could tag the package with ```non-compliant-license``` for further review: +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. -Screenshot 2025-07-30 at 14 21 19 +--- -Screenshot 2025-07-30 at 14 28 43 +## 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 -### 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 -``` +These policies are intended to be readable, predictable, and suitable for enterprise environments. -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```: +## Repository Structure ``` -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" +baseline/ +advanced/ +legacy/ +exemptions/ + allow.json + update_policy.py + templates/ + allowlist.rego.tpl +.github/workflows/ + opa-lint.yml + apply-exemptions.yml ``` -*** +### baseline/ -### 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 -``` +Recommended secure defaults for production environments. -If a package has ```upstream``` and not ```approved``` --> allowed -

-If a package has ```approved``` --> blocked (even if upstream is present) +These policies address common supply chain security requirements such as: +- Malware blocking +- High-risk vulnerability control (CVSS / EPSS) +- License compliance +- Workflows using package age +- Explicit allowlist and blocklist handling -*** +If you are deploying EPM in a new workspace, start here. -### 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. +### advanced/ -``` -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" -``` +Optional or format-specific policies that provide deeper governance controls. -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: +These may include: -Screenshot 2025-07-28 at 10 28 21 +- Base image origin enforcement +- SBOM-based controls +- Model governance policies +- Specialized workflow patterns +Advanced policies are production-ready but not universally required. -*** +--- -### 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 -``` +### legacy/ -This CVE was published on 24 April 2025 - much older than the 30 day threshold specified in the policy. +Historical recipes and experimental policies retained for reference. -``` -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" -``` - -*** +Policies in this directory: +- May use older patterns +- May not reflect current schema or best practices +- Are not recommended for new deployments -### 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 -``` +They are preserved for documentation history and migration reference. -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 +### exemptions/ -Screenshot 2025-07-28 at 11 26 50 +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 by GitHub Actions when changes are pushed to the `epm-baseline-refactor` branch. -*** +See the [Managing Exemptions](#managing-exemptions-gitops-workflow) section for details. +--- -### 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 -``` +## Policy Ordering & Precedence -*** +Cloudsmith EPM evaluates policies in precedence order (lowest precedence runs first). -### 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 -``` +All policies in this repository are designed to be non-terminal and composable. -``` -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" -``` +A recommended precedence pattern for baseline deployments is: -Screenshot 2025-07-29 at 13 43 39 +1. Package age restore (make eligible packages available again) +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) +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. -*** +For full EPM documentation, see: +https://docs.cloudsmith.com/supply-chain-security/epm +--- -### 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 -``` +## Managing Exemptions (GitOps Workflow) -``` -pip download -cloudsmith push python $CLOUDSMITH_ORG/$CLOUDSMITH_REPO .whl -k "$CLOUDSMITH_API_KEY" -``` +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`: -### 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 +```json +[ + "python:requests:2.6.4", + "npm:left-pad:1.3.0" +] ``` -**Trade-off:** While the tag remains, new CVEs on that package won’t trigger quarantine. You'll need to review those tags regularly. +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 -### 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. - -*** +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. -### 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 -``` -*** +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. -### 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 +## Deployment -This policy lets you maintain an **exact deny list** of suspicious or malicious packages across formats using the key pattern: +Policies can be deployed using the Cloudsmith API or CLI. -`::` (for example: `npm:@alloc/quick-lru:5.2.0`, `python:requests:2.6.0`). +Refer to the official documentation for EPM policy management and simulation: -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. +https://docs.cloudsmith.com/supply-chain-security/epm -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 +This repository is the single source of truth for: -### Hugging Face recipes +- Policy templates +- Documentation examples +- Secure baseline recommendations +- Enterprise EPM enablement guidance -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/). 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/advanced/huggingface-recipes/model_card.rego b/advanced/huggingface-recipes/model_card.rego new file mode 100644 index 0000000..ad540cd --- /dev/null +++ b/advanced/huggingface-recipes/model_card.rego @@ -0,0 +1,12 @@ +package cloudsmith + +default match := false + +pkg := input.v0.package + +hf_pkg if pkg.format == "huggingface" + +match if { + hf_pkg + "HuggingFaceTB/smollm-corpus" in pkg.card.datasets +} 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/huggingface-recipes/risky_files.rego b/advanced/huggingface-recipes/risky_files.rego similarity index 55% rename from huggingface-recipes/risky_files.rego rename to advanced/huggingface-recipes/risky_files.rego index fce69a8..2e81dc1 100644 --- a/huggingface-recipes/risky_files.rego +++ b/advanced/huggingface-recipes/risky_files.rego @@ -1,12 +1,10 @@ package cloudsmith -import rego.v1 - 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" @@ -22,11 +20,16 @@ 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 - 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/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/huggingface-recipes/security_scan.rego b/advanced/huggingface-recipes/security_scan.rego similarity index 64% rename from huggingface-recipes/security_scan.rego rename to advanced/huggingface-recipes/security_scan.rego index b239629..8c4d5b3 100644 --- a/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 @@ -11,15 +9,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 + input.v0.package.format == "huggingface" + is_upstream_pkg + incomplete_or_unsafe } 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/huggingface-recipes/trusted_publishers.rego b/advanced/huggingface-recipes/trusted_publishers.rego similarity index 74% rename from huggingface-recipes/trusted_publishers.rego rename to advanced/huggingface-recipes/trusted_publishers.rego index dc05d6c..3570ab7 100644 --- a/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 @@ -12,7 +10,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 + input.v0.package.format == "huggingface" + is_upstream_pkg + publisher in verified_publishers } 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.rego b/baseline/exact-allowlist-exemption.rego new file mode 100644 index 0000000..1932118 --- /dev/null +++ b/baseline/exact-allowlist-exemption.rego @@ -0,0 +1,27 @@ +package cloudsmith + +default match := false + +pkg := input.v0.package + +allowlist := { + "python:example-lib:1.2.3", + "npm:example-ui:4.5.6", +} + +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/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.rego b/baseline/exact-blocklist.rego new file mode 100644 index 0000000..2ffea4d --- /dev/null +++ b/baseline/exact-blocklist.rego @@ -0,0 +1,27 @@ +package cloudsmith + +default match := false + +pkg := input.v0.package + +blocklist := { + "python:malicious-lib:0.1.0", + "npm:compromised-ui:9.9.9", +} + +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 +} + +reason[msg] if { + match + msg := sprintf( + "Blocked by explicit deny list: %s", + [pkg_key], + ) +} 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 new file mode 100644 index 0000000..22524a8 --- /dev/null +++ b/baseline/high-risk-vulnerability.rego @@ -0,0 +1,82 @@ +# 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 + +min_cvss := 7.0 + +ignored := {} + +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 contains k if { + some k, _ in aliased_records + not k in ignored + max_cvss_for_key(k) >= min_cvss + has_fix_for_key(k) +} + +cvss_scores_for_record(v) := top_level | affected_level if { + top_level := {sev.numerical_score | + some sev in v.severity + sev.numerical_score != null + } + affected_level := {sev.numerical_score | + some a in v.affected + some sev in a.severity + sev.numerical_score != null + } +} + +max_cvss_for_key(k) := max(scores) if { + scores := {s | + some v in aliased_records[k] + some s in cvss_scores_for_record(v) + } + count(scores) > 0 +} else := 0.0 + +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 + 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) + 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..f1e89c8 --- /dev/null +++ b/baseline/high-risk-vulnerability_test.rego @@ -0,0 +1,101 @@ +package cloudsmith_test + +# ── 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.rego b/baseline/license-compliance.rego new file mode 100644 index 0000000..6a9a38b --- /dev/null +++ b/baseline/license-compliance.rego @@ -0,0 +1,55 @@ +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", + "SSPL-1.0", +} + +match if { + 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 +} + +reason[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], + ) +} 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.rego b/baseline/malware-block.rego new file mode 100644 index 0000000..94a8d00 --- /dev/null +++ b/baseline/malware-block.rego @@ -0,0 +1,22 @@ +package cloudsmith + +default match := false + +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[msg] if { + match + msg := sprintf( + "Detected %d malicious vulnerability ID(s): %v", + [count(malicious_packages), malicious_packages], + ) +} 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.rego b/baseline/package-age-quarantine.rego new file mode 100644 index 0000000..e4d33ea --- /dev/null +++ b/baseline/package-age-quarantine.rego @@ -0,0 +1,29 @@ +package cloudsmith + +default match := false + +pkg := input.v0.package + +within_past_days := 3 + +match if below_minimum_release_age + +below_minimum_release_age if { + pkg.upstream_metadata != null + pkg.upstream_metadata.published_at != null + + publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) + + days_ago := 0 - within_past_days + cutoff := time.add_date(time.now_ns(), 0, 0, days_ago) + + publish_date >= cutoff +} + +reason[msg] if { + below_minimum_release_age + msg := sprintf( + "Package published within last %v days — below minimum release age", + [within_past_days], + ) +} 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.rego b/baseline/package-age-restore.rego new file mode 100644 index 0000000..0b30ce3 --- /dev/null +++ b/baseline/package-age-restore.rego @@ -0,0 +1,29 @@ +package cloudsmith + +default match := false + +pkg := input.v0.package + +within_past_days := 3 + +match if above_minimum_release_age + +above_minimum_release_age if { + pkg.upstream_metadata != null + pkg.upstream_metadata.published_at != null + + publish_date := time.parse_rfc3339_ns(pkg.upstream_metadata.published_at) + + days_ago := 0 - within_past_days + cutoff := time.add_date(time.now_ns(), 0, 0, days_ago) + + publish_date < cutoff +} + +reason[msg] if { + above_minimum_release_age + msg := sprintf( + "Package older than %v days — meets minimum release age", + [within_past_days], + ) +} 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 +} diff --git a/exemptions/allow.json b/exemptions/allow.json new file mode 100644 index 0000000..d2b1bb3 --- /dev/null +++ b/exemptions/allow.json @@ -0,0 +1,11 @@ +[ + "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", + "python:time:3.4.5"] 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..6a08213 --- /dev/null +++ b/exemptions/update_policy.py @@ -0,0 +1,147 @@ +import os +import json +import requests +from pathlib import Path +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +# -------------------------------------------------- +# 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", +} + +# 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 +# -------------------------------------------------- + +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 _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() + + # 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) + + +# -------------------------------------------------- +# CLOUDSMITH API +# -------------------------------------------------- + +def fetch_policy(): + r = _session.get(POLICY_URL, headers=HEADERS, timeout=REQUEST_TIMEOUT) + 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 = _session.put(POLICY_URL, headers=HEADERS, json=payload, timeout=REQUEST_TIMEOUT) + 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() diff --git a/huggingface-recipes/model_card.rego b/huggingface-recipes/model_card.rego deleted file mode 100644 index 3d412fb..0000000 --- a/huggingface-recipes/model_card.rego +++ /dev/null @@ -1,14 +0,0 @@ -package cloudsmith - -import rego.v1 - -default match := false - -pkg := input.v0.package - -hf_pkg if "huggingface" == pkg.format - -match if { - hf_pkg - "HuggingFaceTB/smollm-corpus" in pkg.card.datasets -} 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