diff --git a/src/pages/explanations/security/dependency-proxy-security.mdx b/src/pages/explanations/security/dependency-proxy-security.mdx index feadd38..9d2ef9c 100644 --- a/src/pages/explanations/security/dependency-proxy-security.mdx +++ b/src/pages/explanations/security/dependency-proxy-security.mdx @@ -21,15 +21,19 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@document-writing-tools ## Overview -The DevGuard Dependency Proxy is a security-focused intermediary that sits between your development environment and public package registries. When you request a package from npm, Go, or PyPI, the request goes through DevGuard's proxy, which performs security checks and caching before delivering the package to you. +The DevGuard Dependency Proxy is a security-focused intermediary that sits between your development environment and public package registries. When you request a package from one of the supported registries, the request goes through DevGuard's proxy, which performs security checks and caching before delivering the package to you. ## Supported Ecosystems -DevGuard currently proxies three major package ecosystems: +DevGuard currently proxies seven package ecosystems: - **npm** (Node.js packages from registry.npmjs.org) - **Go** (Go modules from proxy.golang.org) - **PyPI** (Python packages from pypi.org) +- **Maven** (Java packages from Maven Central) +- **Composer** (PHP packages from Packagist) +- **Debian** (apt packages from deb.debian.org) +- **OCI** (container images from public registries) ## The Request Flow @@ -53,8 +57,11 @@ If the package is flagged as malicious: - The incident is logged and metrics are updated - Any existing cache of the package is purged -### 4. Cache Lookup -If the package passes the security check, DevGuard looks in its local cache. The cache system uses different time-to-live (TTL) values based on content type: +### 4. Package Rules +Next, the [package rules](/how-to-guides/dependency-proxy/package-rules) of the organization, project or repository are evaluated against the package — for example its name, version, release time and, for npm, its publisher. The last matching rule decides whether the package is allowed or denied. Denied packages are rejected with `403 Forbidden` and the name of the rule; for metadata requests, denied versions are removed from the version list instead. + +### 5. Cache Lookup +If the package passes these checks, DevGuard looks in its local cache. The cache system uses different time-to-live (TTL) values based on content type: **For npm:** - Package tarballs are cached for 24 hours @@ -68,13 +75,13 @@ If the package passes the security check, DevGuard looks in its local cache. The - Distribution files (wheels and source) are cached for 7 days - Package metadata is cached for 1 hour -### 5. Integrity Verification +### 6. Integrity Verification When serving from cache, DevGuard verifies the integrity of the cached data using SHA256 checksums. Each cached file has an associated `.sha256` file containing its hash. If verification fails, the corrupted cache is discarded and the package is refetched. -### 6. Upstream Fetch +### 7. Upstream Fetch If there's a cache miss or the cache is expired, DevGuard fetches the package from the upstream registry. During this fetch, it forwards appropriate headers to ensure compatibility with registry requirements. -### 7. Version Resolution (npm-specific) +### 8. Version Resolution (npm-specific) For npm metadata requests that don't specify a version, DevGuard performs an additional security step: - It fetches the package metadata - Extracts the "latest" version from the `dist-tags` @@ -83,10 +90,10 @@ For npm metadata requests that don't specify a version, DevGuard performs an add This prevents scenarios where a developer runs `npm install package` (without version) and unknowingly gets a malicious latest version. -### 8. Cache Storage +### 9. Cache Storage Successful responses from upstream are stored in the cache along with their SHA256 integrity hash. The cache directory structure mirrors the package path for easy organization and cleanup. -### 9. Response Delivery +### 10. Response Delivery Finally, DevGuard returns the package to the client with additional headers indicating: - Cache status (`X-Cache: HIT` or `X-Cache: MISS`) - Proxy type (`X-Proxy-Type: npm/go/pypi`) diff --git a/src/pages/how-to-guides/dependency-proxy/_meta.ts b/src/pages/how-to-guides/dependency-proxy/_meta.ts index 6ac141b..e84e6b9 100644 --- a/src/pages/how-to-guides/dependency-proxy/_meta.ts +++ b/src/pages/how-to-guides/dependency-proxy/_meta.ts @@ -1,5 +1,6 @@ export default { index: { title: 'Overview' }, + 'package-rules': { title: 'Package Rules' }, 'cache-management': { title: 'Cache Management' }, 'setup-go-proxy': { title: 'Setup Go Proxy' }, 'setup-npm-proxy': { title: 'Setup NPM Proxy' }, diff --git a/src/pages/how-to-guides/dependency-proxy/ci-runners.mdx b/src/pages/how-to-guides/dependency-proxy/ci-runners.mdx index 75d0556..27325f0 100644 --- a/src/pages/how-to-guides/dependency-proxy/ci-runners.mdx +++ b/src/pages/how-to-guides/dependency-proxy/ci-runners.mdx @@ -29,7 +29,7 @@ The proxy URLs in the runner configuration contain a dependency proxy secret: | Go modules | `GOPROXY` | `https:///api/v1/dependency-proxy//go/` | | PyPI | `PIP_INDEX_URL` | `https:///api/v1/dependency-proxy//pypi/simple/` | -The secret links the requests to your organization, project or repository in DevGuard, so the proxy applies the rules and the [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) configured there. Requests without a secret are only checked against the malicious package database. +The secret links the requests to your organization, project or repository in DevGuard, so the proxy applies the [package rules](/how-to-guides/dependency-proxy/package-rules) configured there. Requests without a secret are only checked against the malicious package database. Treat the secret like a credential. Store the runner configuration in a Kubernetes secret or a sealed/encrypted secret instead of committing it in plain text. diff --git a/src/pages/how-to-guides/dependency-proxy/index.mdx b/src/pages/how-to-guides/dependency-proxy/index.mdx index 102cb15..21c3901 100644 --- a/src/pages/how-to-guides/dependency-proxy/index.mdx +++ b/src/pages/how-to-guides/dependency-proxy/index.mdx @@ -22,8 +22,9 @@ DevGuard includes a built-in dependency proxy that acts as a protective layer be 1. **Request Interception**: When you install a package, the proxy intercepts the request 2. **Database Check**: The package is checked against the [OSV dataset](https://osv.dev), which includes GitHub malware advisories — kept current via the [vulndb sync](/reference/vulnerability-database/update-schedule) 3. **Blocking**: If flagged as malicious, the request is blocked and logged -4. **Caching**: Safe packages are cached with SHA256 integrity verification -5. **Metrics**: All blocked attempts are recorded in Prometheus metrics +4. **Package Rules**: Your [package rules](/how-to-guides/dependency-proxy/package-rules) decide whether the package is allowed, e.g. based on its release age +5. **Caching**: Safe packages are cached with SHA256 integrity verification +6. **Metrics**: All blocked attempts are recorded in Prometheus metrics ## Supported Ecosystems @@ -49,21 +50,22 @@ For setup instructions, see the ecosystem-specific guides: To route all CI jobs through the proxy at once, see [Self-Hosted CI Runners](/how-to-guides/dependency-proxy/ci-runners). -## Minimum Package Age +## Package Rules -The proxy can be configured to refuse any package version published more recently than a specified number of hours. This gives the security community time to detect and flag malicious releases before they reach your teams — a simple but effective defense against attacks that rely on packages being downloaded within hours of publication. +Package rules allow or deny packages with [CEL](https://github.com/google/cel-spec) expressions. They are configured per organization, project or repository and apply to requests using its proxy URL, which contains a secret. Rules are evaluated top to bottom and the last matching rule wins. -The minimum age is configured per organization, project or repository and applies to requests using its proxy URL, which contains a secret. Versions that are too new are hidden from the package metadata, so version ranges resolve to the newest version that is old enough; a download of a too new version is rejected with `403 Forbidden`. +A minimum release age is the most common rule. It refuses versions published more recently than a given time, which gives the security community time to detect and flag malicious releases before they reach your teams: -| Ecosystem | Minimum package age | -|-----------|---------------------| -| npm | Supported | -| PyPI | Supported | -| Debian (apt) | Supported — too new versions are rejected but not hidden, see [Setup Debian Proxy](/how-to-guides/dependency-proxy/setup-debian-proxy#blocked-packages-and-signed-package-lists) | -| Go modules | Supported | -| Maven | Supported | -| Composer (Packagist) | Supported | -| OCI (container images) | Not supported — registries expose no reliable publish date, see [Setup OCI Proxy](/how-to-guides/dependency-proxy/setup-oci-proxy) | +```yaml +rules: + - name: min-release-age + action: deny + expr: pkg.ecosystem != "oci" && now() - timestamp(pkg.releasedAt) < duration("120h") +``` + +Versions that are blocked are hidden from the package metadata, so version ranges resolve to an allowed version; a download of a blocked version is rejected with `403 Forbidden`. Debian package lists are signed and cannot be filtered, see [Setup Debian Proxy](/how-to-guides/dependency-proxy/setup-debian-proxy#blocked-packages-and-signed-package-lists). OCI registries expose no reliable publish date, so the release age cannot be checked for container images. + +See [Package Rules](/how-to-guides/dependency-proxy/package-rules) for the available fields, functions and more examples. ## Cache & Security diff --git a/src/pages/how-to-guides/dependency-proxy/package-rules.mdx b/src/pages/how-to-guides/dependency-proxy/package-rules.mdx new file mode 100644 index 0000000..29fac22 --- /dev/null +++ b/src/pages/how-to-guides/dependency-proxy/package-rules.mdx @@ -0,0 +1,180 @@ +--- +title: Dependency Proxy Package Rules +description: "Allow or deny packages in the DevGuard dependency proxy with CEL rules — minimum release age, blocked scopes, publisher changes and exceptions." +seo: + robots: index,follow + og: + image: /og-image.png + type: article + schema: + type: TechArticle + keyword_primary: dependency proxy package rules +lang: en-US +ignoreChecks: null +--- + +import { Callout } from '@document-writing-tools/kernux-theme' + +# Dependency Proxy Package Rules + +Package rules decide which packages the [dependency proxy](/how-to-guides/dependency-proxy) serves. Each rule is a +[CEL (Common Expression Language)](https://github.com/google/cel-spec) expression that is evaluated against the +requested package. Rules are configured per organization, project or repository in the **Dependency Proxy** +settings and apply to requests using the proxy URL of that scope. + +The malicious package check always runs, independently of your rules — an `allow` rule can never let a known +malicious package through. + +## Format + +Rules are written in YAML: + +```yaml +rules: + - name: min-release-age + action: deny + expr: pkg.ecosystem != "oci" && now() - timestamp(pkg.releasedAt) < duration("120h") + - name: allow-trusted + action: allow + expr: pkg.name == "my-trusted-package" +``` + +| Field | Description | +|-------|-------------| +| `name` | Shown in the error message when the rule blocks a package. | +| `action` | `deny` blocks a matching package, `allow` lets it through. | +| `expr` | A CEL expression that evaluates to a boolean. `true` means the rule matches. | + +## Evaluation + +- Rules are evaluated **top to bottom** and the **last matching rule wins**. Put general rules first and exceptions below them. +- A package that matches no rule is **allowed**. +- A configuration with invalid YAML, an unknown `action` or an expression that does not compile is rejected when saving. +- If an expression fails at runtime — for example because it reads a field the package does not have — the package is **blocked** (fail closed). + +A blocked download is answered with `403 Forbidden`, the header `X-Not-Allowed-Package: blocked` and the name of the rule. +Blocked versions are also hidden from the version lists, so version ranges resolve to an allowed version. npm, PyPI +and Composer check every version; Go and Maven check from the newest version downwards until the first allowed one, +because each check needs an extra request for the release time. Debian package lists are signed and cannot be filtered, see +[Setup Debian Proxy](/how-to-guides/dependency-proxy/setup-debian-proxy#blocked-packages-and-signed-package-lists). + +## The `pkg` variable + +| Field | Type | Description | +|-------|------|-------------| +| `pkg.name` | string | Package name, e.g. `lodash`, `@babel/core`, `github.com/gin-gonic/gin` | +| `pkg.version` | string | Requested version | +| `pkg.ecosystem` | string | `npm`, `go`, `pypi`, `maven`, `composer`, `deb` or `oci` | +| `pkg.purl` | string | e.g. `pkg:npm/lodash@4.17.21`; OCI uses the image reference, e.g. `docker.io/library/nginx:latest` | +| `pkg.registry` | string | Upstream registry, e.g. `https://pypi.org` | +| `pkg.releasedAt` | string (RFC 3339) | Publish time of the version. Use `timestamp(pkg.releasedAt)` to compare it. | +| `pkg.publisher.name` | string | User who published this version (npm only) | +| `pkg.publisher.changedSinceLastVersion` | bool | The publisher differs from the one of the previously published version (npm only) | + +Not every ecosystem provides every field: + +| Ecosystem | Source of `releasedAt` | `publisher` | +|-----------|------------------------|-------------| +| npm | `time` field of the package metadata | ✓ | +| Go modules | `Time` field of the `.info` file | – | +| PyPI | Upload time of the file | – | +| Composer | `time` field reported by Packagist | – | +| Maven | `Last-Modified` header from Maven Central | – | +| Debian | `Last-Modified` header from the mirror | – | +| OCI | not available | – | + +### Missing fields + +Fields that are not available are absent, not empty. Reading an absent field makes the expression fail, which +blocks the package. Use `has()` to decide explicitly what happens: + +```yaml +# allow packages without a release time +expr: has(pkg.releasedAt) && now() - timestamp(pkg.releasedAt) < duration("120h") + +# block packages without a release time +expr: '!has(pkg.releasedAt) || now() - timestamp(pkg.releasedAt) < duration("120h")' +``` + +Expressions that start with `!` must be quoted, otherwise YAML reads the `!` as a tag. + + + OCI images never have a release time. A minimum age rule without `has()` or an `pkg.ecosystem != "oci"` check + blocks every image. + + +## Functions + +| Function | Example | +|----------|---------| +| `now()` | Current time as timestamp | +| `timestamp(string)` | `timestamp(pkg.releasedAt)` | +| `duration(string)` | `duration("120h")` — hours, minutes or seconds (`h`, `m`, `s`) | +| `has(field)` | `has(pkg.publisher)` | +| `startsWith`, `endsWith`, `contains`, `matches` | `pkg.name.startsWith("@evil-scope/")`, `pkg.name.matches("^left-?pad$")` | +| `in` | `pkg.ecosystem in ["npm", "pypi"]` | + +All standard CEL operators (`==`, `!=`, `<`, `&&`, `||`, `!`) are available. + +## Cookbook + +### Minimum release age + +```yaml +- name: min-release-age + action: deny + expr: pkg.ecosystem != "oci" && now() - timestamp(pkg.releasedAt) < duration("120h") +``` + +### Exception for one package + +Place it **below** the rule it overrides. + +```yaml +- name: allow-lodash + action: allow + expr: pkg.name == "lodash" +``` + +### Block a whole npm scope + +```yaml +- name: block-evil-scope + action: deny + expr: pkg.name.startsWith("@evil-scope/") +``` + +### Block one exact version + +```yaml +- name: block-lodash-4-17-21 + action: deny + expr: pkg.purl == "pkg:npm/lodash@4.17.21" +``` + +### Block versions published by a new maintainer + +Account takeovers often publish a new version from a different account. + +```yaml +- name: publisher-changed + action: deny + expr: has(pkg.publisher) && pkg.publisher.changedSinceLastVersion +``` + +### Allow only one ecosystem + +```yaml +- name: deny-all + action: deny + expr: "true" +- name: allow-npm + action: allow + expr: pkg.ecosystem == "npm" +``` + +## Related Documentation + +- [Dependency Proxy Overview](/how-to-guides/dependency-proxy) — how the proxy works +- [Dependency Proxy Security](/explanations/security/dependency-proxy-security) — where rules fit into the request flow +- [CEL Expression Reference for VEX Rules](/reference/vex-cel-reference) — CEL in VEX rules diff --git a/src/pages/how-to-guides/dependency-proxy/setup-composer-proxy.mdx b/src/pages/how-to-guides/dependency-proxy/setup-composer-proxy.mdx index 868f2d9..70369f4 100644 --- a/src/pages/how-to-guides/dependency-proxy/setup-composer-proxy.mdx +++ b/src/pages/how-to-guides/dependency-proxy/setup-composer-proxy.mdx @@ -21,7 +21,7 @@ PHP projects are just as exposed to supply chain attacks as any other ecosystem. Composer and Packagist have responded with [several hardening measures](https://blog.packagist.com/an-update-on-composer-packagist-supply-chain-security/): Packagist flags malware versions and makes [stable versions immutable](https://blog.packagist.com/immutable-versions-on-packagist/), and [Composer 2.10](https://blog.packagist.com/composer-2-10-release/) introduced the [`policy` config](https://getcomposer.org/doc/06-config.md#policy), which blocks flagged malware versions, security advisories and abandoned packages by default. These measures only apply to clients running a recent Composer version, and each project can relax or disable them in its own `composer.json`. -The DevGuard dependency proxy sits between Composer and Packagist and enforces protection centrally, for every Composer version and without any client configuration. Every package request is checked against the [OSV dataset](https://osv.dev) before it reaches your `vendor` directory, blocking known malicious packages automatically. Combined with the [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age), the proxy complements Composer's own policies rather than replacing them. +The DevGuard dependency proxy sits between Composer and Packagist and enforces protection centrally, for every Composer version and without any client configuration. Every package request is checked against the [OSV dataset](https://osv.dev) before it reaches your `vendor` directory, blocking known malicious packages automatically. Combined with [package rules](/how-to-guides/dependency-proxy/package-rules), the proxy complements Composer's own policies rather than replacing them. - **Registry URL**: `/api/v1/dependency-proxy/composer` @@ -106,20 +106,23 @@ done If the install is blocked, the proxy is working correctly. Composer reports the `403 Forbidden` for the package's metadata file. All other packages resolve normally from Packagist. -### Testing the minimum package age +### Testing a minimum release age rule -The [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. The proxy uses the release time Packagist reports for each version: versions that are too new are removed from the package metadata, so Composer resolves to the newest version that is old enough, and downloads of a too new version are rejected. +A [minimum release age rule](/how-to-guides/dependency-proxy/package-rules#minimum-release-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. The proxy uses the release time Packagist reports for each version: versions that are too new are removed from the package metadata, so Composer resolves to the newest version that is old enough, and downloads of a too new version are rejected. -To verify it, temporarily set the minimum age to 87600 hours (10 years). `monolog/monolog` 3.7.0 was published in June 2024, so the proxy must reject it, while `composer require` without a version still resolves to an older version: +To verify it, temporarily add a rule that denies everything released in the last 87600 hours (10 years). `monolog/monolog` 3.7.0 was published in June 2024, so the proxy must reject it, while `composer require` without a version still resolves to an older version: The proxy enforces the minimum age centrally, for every client and without any client configuration. ```bash -# set the minimum package age of the repository to 10 years +# deny every package version released in the last 10 years devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":87600}' \ + -d 'rules: + - name: min-release-age + action: deny + expr: now() - timestamp(pkg.releasedAt) < duration("87600h")' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ # the repository's Composer proxy URL applies its settings @@ -137,7 +140,7 @@ composer config repo.devguard composer "$COMPOSER_PROXY" # monolog/monolog 3.7.0 is younger than 10 years and must be rejected if composer require monolog/monolog:3.7.0; then - echo "monolog/monolog 3.7.0 was NOT blocked by the minimum package age" >&2 + echo "monolog/monolog 3.7.0 was NOT blocked by the minimum release age rule" >&2 exit 1 fi @@ -153,9 +156,9 @@ fi composer require monolog/monolog composer show monolog/monolog | grep versions -# reset the minimum package age +# remove the rule again devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":0}' \ + -d 'rules: []' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ ``` diff --git a/src/pages/how-to-guides/dependency-proxy/setup-debian-proxy.mdx b/src/pages/how-to-guides/dependency-proxy/setup-debian-proxy.mdx index 52eeaca..01a1746 100644 --- a/src/pages/how-to-guides/dependency-proxy/setup-debian-proxy.mdx +++ b/src/pages/how-to-guides/dependency-proxy/setup-debian-proxy.mdx @@ -70,7 +70,7 @@ Once set, all `apt-get update`, `apt-get install` and `apt-get upgrade` invocati For npm, Go and PyPI, the proxy removes blocked versions from the version list, so the client resolves to the next allowed version. This is not possible for Debian: the package lists are signed through `InRelease`, and any change would break the signature check of apt. -apt therefore always sees the newest version, tries to download it, receives `403 Forbidden` and aborts. This applies to malicious packages and to the [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) alike. To install an older version, pin it explicitly, for example with `apt-get install =`, as long as that version is still available in the archive. +apt therefore always sees the newest version, tries to download it, receives `403 Forbidden` and aborts. This applies to malicious packages and to [package rules](/how-to-guides/dependency-proxy/package-rules) alike. To install an older version, pin it explicitly, for example with `apt-get install =`, as long as that version is still available in the archive. ## Testing @@ -98,16 +98,19 @@ fi `fake-malicious-package` does not exist in the Debian archive, so apt cannot request it. The proxy checks a download against the malicious package database before fetching it from upstream, so the `curl` request above proves the check without a real package. -### Testing the minimum package age +### Testing a minimum release age rule -The [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. The proxy uses the upload time the Debian archive reports for each `.deb` file. +A [minimum release age rule](/how-to-guides/dependency-proxy/package-rules#minimum-release-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. The proxy uses the upload time the Debian archive reports for each `.deb` file. -To verify it, temporarily set the minimum age to 87600 hours (10 years). `hello` 2.10-5 was uploaded to trixie in April 2025, so the proxy must reject it. As described [above](#blocked-packages-and-signed-package-lists), apt does not fall back to an older version but aborts: +To verify it, temporarily add a rule that denies everything released in the last 87600 hours (10 years). `hello` 2.10-5 was uploaded to trixie in April 2025, so the proxy must reject it. As described [above](#blocked-packages-and-signed-package-lists), apt does not fall back to an older version but aborts: ```bash -# set the minimum package age of the repository to 10 years +# deny every package version released in the last 10 years devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":87600}' \ + -d 'rules: + - name: min-release-age + action: deny + expr: now() - timestamp(pkg.releasedAt) < duration("87600h")' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ # the repository's Debian proxy URL applies its settings @@ -121,16 +124,16 @@ docker run --rm -e DEVGUARD_DEB="${DEBIAN_PROXY%/debian}" debian:trixie-slim bas apt-get update if apt-get install -y --no-install-recommends hello > apt.log 2>&1; then cat apt.log - echo "hello was NOT blocked by the minimum package age" >&2 + echo "hello was NOT blocked by the minimum release age rule" >&2 exit 1 fi cat apt.log grep -q "403" apt.log ' -# reset the minimum package age +# remove the rule again devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":0}' \ + -d 'rules: []' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ ``` diff --git a/src/pages/how-to-guides/dependency-proxy/setup-go-proxy.mdx b/src/pages/how-to-guides/dependency-proxy/setup-go-proxy.mdx index 8354362..796c30c 100644 --- a/src/pages/how-to-guides/dependency-proxy/setup-go-proxy.mdx +++ b/src/pages/how-to-guides/dependency-proxy/setup-go-proxy.mdx @@ -87,11 +87,11 @@ fi Checksum verification keeps working behind the proxy: `go` looks up the checksums of downloaded modules in the [Go checksum database](https://sum.golang.org), and DevGuard forwards these requests unchanged. -### Testing the minimum package age +### Testing a minimum release age rule -The [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. The proxy removes versions that are too new from the version list, so `go get` resolves to the newest version that is old enough. Downloads of a too new version (`.info`, `.mod`, `.zip`) are rejected. +A [minimum release age rule](/how-to-guides/dependency-proxy/package-rules#minimum-release-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. The proxy removes versions that are too new from the version list, so `go get` resolves to the newest version that is old enough. Downloads of a too new version (`.info`, `.mod`, `.zip`) are rejected. -To verify it, temporarily set the minimum age to 87600 hours (10 years). `github.com/pkg/errors@v0.9.1` was published in January 2020, so the proxy must reject it, while `go get` without a version still resolves to an older version: +To verify it, temporarily add a rule that denies everything released in the last 87600 hours (10 years). `github.com/pkg/errors@v0.9.1` was published in January 2020, so the proxy must reject it, while `go get` without a version still resolves to an older version: ```bash # use a fresh module and module cache, so no downloaded version is reused @@ -99,9 +99,12 @@ cd .. && mkdir -p go-min-age-test && cd go-min-age-test go mod init example.com/go-min-age-test export GOMODCACHE="$(pwd)/.gomodcache" -# set the minimum package age of the repository to 10 years +# deny every package version released in the last 10 years devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":87600}' \ + -d 'rules: + - name: min-release-age + action: deny + expr: now() - timestamp(pkg.releasedAt) < duration("87600h")' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ # the repository's Go proxy URL applies its settings @@ -111,7 +114,7 @@ GO_PROXY=$(devguard-scanner curl --token -s \ # github.com/pkg/errors@v0.9.1 is younger than 10 years and must be rejected if GOPROXY="$GO_PROXY" go get github.com/pkg/errors@v0.9.1; then - echo "github.com/pkg/errors@v0.9.1 was NOT blocked by the minimum package age" >&2 + echo "github.com/pkg/errors@v0.9.1 was NOT blocked by the minimum release age rule" >&2 exit 1 fi @@ -119,9 +122,9 @@ fi GOPROXY="$GO_PROXY" go get github.com/pkg/errors go list -m github.com/pkg/errors -# reset the minimum package age +# remove the rule again devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":0}' \ + -d 'rules: []' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ ``` diff --git a/src/pages/how-to-guides/dependency-proxy/setup-maven-proxy.mdx b/src/pages/how-to-guides/dependency-proxy/setup-maven-proxy.mdx index 3942a3b..bdce7f1 100644 --- a/src/pages/how-to-guides/dependency-proxy/setup-maven-proxy.mdx +++ b/src/pages/how-to-guides/dependency-proxy/setup-maven-proxy.mdx @@ -111,20 +111,23 @@ If the download is blocked, the proxy is working correctly. Maven reports the `4 Checksum verification keeps working behind the proxy: Maven downloads the `.sha1` and `.md5` files of every artifact through DevGuard and verifies them as usual. -### Testing the minimum package age +### Testing a minimum release age rule -The [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. Maven Central has no publish date API, so the proxy uses the upload time Maven Central reports for each file. Versions that are too new are removed from `maven-metadata.xml`, and `` and `` point to the newest version that is old enough. Downloads of a too new version are rejected. +A [minimum release age rule](/how-to-guides/dependency-proxy/package-rules#minimum-release-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. Maven Central has no publish date API, so the proxy uses the upload time Maven Central reports for each file. Versions that are too new are removed from `maven-metadata.xml`, and `` and `` point to the newest version that is old enough. Downloads of a too new version are rejected. -To verify it, temporarily set the minimum age to 87600 hours (10 years). `com.google.code.gson:gson:2.11.0` was published in May 2024, so the proxy must reject it, while `RELEASE` still resolves to an older version: +To verify it, temporarily add a rule that denies everything released in the last 87600 hours (10 years). `com.google.code.gson:gson:2.11.0` was published in May 2024, so the proxy must reject it, while `RELEASE` still resolves to an older version: Maven has no client-side minimum release age. The proxy enforces it centrally, for every build and without any client configuration. ```bash -# set the minimum package age of the repository to 10 years +# deny every package version released in the last 10 years devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":87600}' \ + -d 'rules: + - name: min-release-age + action: deny + expr: now() - timestamp(pkg.releasedAt) < duration("87600h")' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ # the repository's Maven proxy URL applies its settings @@ -149,7 +152,7 @@ EOF # gson 2.11.0 is younger than 10 years and must be rejected if mvn -B -s settings-min-age.xml org.apache.maven.plugins:maven-dependency-plugin:3.8.1:get \ -Dartifact=com.google.code.gson:gson:2.11.0; then - echo "com.google.code.gson:gson:2.11.0 was NOT blocked by the minimum package age" >&2 + echo "com.google.code.gson:gson:2.11.0 was NOT blocked by the minimum release age rule" >&2 exit 1 fi @@ -166,9 +169,9 @@ mvn -B -s settings-min-age.xml org.apache.maven.plugins:maven-dependency-plugin: -Dartifact=com.google.code.gson:gson:RELEASE ls .m2/repository/com/google/code/gson/gson/ -# reset the minimum package age +# remove the rule again devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":0}' \ + -d 'rules: []' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ ``` diff --git a/src/pages/how-to-guides/dependency-proxy/setup-npm-proxy.mdx b/src/pages/how-to-guides/dependency-proxy/setup-npm-proxy.mdx index 5a017a3..79c6dce 100644 --- a/src/pages/how-to-guides/dependency-proxy/setup-npm-proxy.mdx +++ b/src/pages/how-to-guides/dependency-proxy/setup-npm-proxy.mdx @@ -104,9 +104,9 @@ if [ "$status" != "403" ]; then fi ``` -### Testing the minimum package age +### Testing a minimum release age rule -The [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. To verify it, temporarily set the minimum age to 87600 hours (10 years). `lodash@4.17.21` was published in February 2021, so the proxy must reject it, while a version range still resolves to an older version: +A [minimum release age rule](/how-to-guides/dependency-proxy/package-rules#minimum-release-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. To verify it, temporarily add a rule that denies everything released in the last 87600 hours (10 years). `lodash@4.17.21` was published in February 2021, so the proxy must reject it, while a version range still resolves to an older version: npm 11 has its own client-side minimum release age (`min-release-age` in `.npmrc`, in days). If it is set, npm hides recent versions itself and reports errors like `No matching version found for lodash@4.17.21 with a date before …` — independent of the proxy. The script passes `--min-release-age=0` so that only the proxy decides which versions are available. Both settings can be combined in practice; the proxy enforces the minimum age centrally, **even for clients that do not support or configure it**. @@ -116,9 +116,12 @@ The [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) i mkdir -p npm-min-age-test && cd npm-min-age-test npm init -y > /dev/null -# set the minimum package age of the repository to 10 years +# deny every package version released in the last 10 years devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":87600}' \ + -d 'rules: + - name: min-release-age + action: deny + expr: now() - timestamp(pkg.releasedAt) < duration("87600h")' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ # the repository's npm proxy URL applies its settings @@ -131,16 +134,16 @@ NPM_PROXY=$(devguard-scanner curl --token -s \ # lodash@4.17.21 is younger than 10 years and must be rejected if npm install lodash@4.17.21 --prefer-online --min-release-age=0 --registry "$NPM_PROXY"; then - echo "lodash@4.17.21 was NOT blocked by the minimum package age" >&2 + echo "lodash@4.17.21 was NOT blocked by the minimum release age rule" >&2 exit 1 fi # a version range resolves to the newest version that is old enough - this will install a really outdated version like 4.16.2 npm install "lodash@^4" --prefer-online --min-release-age=0 --registry "$NPM_PROXY" -# reset the minimum package age +# remove the rule again devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":0}' \ + -d 'rules: []' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ ``` diff --git a/src/pages/how-to-guides/dependency-proxy/setup-oci-proxy.mdx b/src/pages/how-to-guides/dependency-proxy/setup-oci-proxy.mdx index 1fe1df3..f9217b6 100644 --- a/src/pages/how-to-guides/dependency-proxy/setup-oci-proxy.mdx +++ b/src/pages/how-to-guides/dependency-proxy/setup-oci-proxy.mdx @@ -47,7 +47,7 @@ docker pull //docker.io/library/nginx:latest ``` - The [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) does not apply to container images. Unlike npm, PyPI and Go, the OCI registry protocol has no publish date for an image: the `created` timestamp inside an image is set by the build and is often fixed to 1970 or missing for reproducible builds, and registry-side push times are only available through registry-specific APIs. Pin images by digest (`image@sha256:…`) to control exactly which image version you pull. + A [minimum release age rule](/how-to-guides/dependency-proxy/package-rules#minimum-release-age) cannot be applied to container images. Unlike npm, PyPI and Go, the OCI registry protocol has no publish date for an image: the `created` timestamp inside an image is set by the build and is often fixed to 1970 or missing for reproducible builds, and registry-side push times are only available through registry-specific APIs. Pin images by digest (`image@sha256:…`) to control exactly which image version you pull. diff --git a/src/pages/how-to-guides/dependency-proxy/setup-pypi-proxy.mdx b/src/pages/how-to-guides/dependency-proxy/setup-pypi-proxy.mdx index 8cd8c78..ec78962 100644 --- a/src/pages/how-to-guides/dependency-proxy/setup-pypi-proxy.mdx +++ b/src/pages/how-to-guides/dependency-proxy/setup-pypi-proxy.mdx @@ -129,11 +129,11 @@ done Thanks a lot to https://github.com/IlyasMakari for bringing this issue to our attention. -### Testing the minimum package age +### Testing a minimum release age rule -The [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. The proxy removes all files that are too new from the package index, so pip resolves to the newest version that is old enough instead of failing on the download. +A [minimum release age rule](/how-to-guides/dependency-proxy/package-rules#minimum-release-age) is configured per repository and only applies to requests using the repository's proxy URL, which contains a secret. The proxy removes all files that are too new from the package index, so pip resolves to the newest version that is old enough instead of failing on the download. -To verify it, temporarily set the minimum age to 87600 hours (10 years). `requests==2.32.3` was published in May 2024, so the proxy must reject it, while an unpinned `requests` still resolves to an older version: +To verify it, temporarily add a rule that denies everything released in the last 87600 hours (10 years). `requests==2.32.3` was published in May 2024, so the proxy must reject it, while an unpinned `requests` still resolves to an older version: Recent pip versions have their own client-side filter, `--uploaded-prior-to`. If it is set in your pip configuration, pip hides recent versions itself, independent of the proxy. The proxy enforces the minimum age centrally, for every client and without any client configuration. @@ -144,9 +144,12 @@ To verify it, temporarily set the minimum age to 87600 hours (10 years). `reques python3 -m venv .venv-min-age . .venv-min-age/bin/activate -# set the minimum package age of the repository to 10 years +# deny every package version released in the last 10 years devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":87600}' \ + -d 'rules: + - name: min-release-age + action: deny + expr: now() - timestamp(pkg.releasedAt) < duration("87600h")' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ # the repository's PyPI proxy URL applies its settings @@ -156,7 +159,7 @@ PYPI_PROXY=$(devguard-scanner curl --token -s \ # requests 2.32.3 is younger than 10 years and must be rejected if pip install --no-cache-dir --index-url "$PYPI_PROXY" "requests==2.32.3"; then - echo "requests==2.32.3 was NOT blocked by the minimum package age" >&2 + echo "requests==2.32.3 was NOT blocked by the minimum release age rule" >&2 exit 1 fi @@ -164,9 +167,9 @@ fi pip install --no-cache-dir --index-url "$PYPI_PROXY" requests python3 -c 'import importlib.metadata as m; print(m.version("requests"))' -# reset the minimum package age +# remove the rule again devguard-scanner curl --token -X PUT \ - -d '{"rules":"","minReleaseAge":0}' \ + -d 'rules: []' \ https:///api/v1/organizations//config-files/dependency-proxy-configs/ deactivate diff --git a/src/pages/how-to-guides/index.mdx b/src/pages/how-to-guides/index.mdx index c6e80ae..618d574 100644 --- a/src/pages/how-to-guides/index.mdx +++ b/src/pages/how-to-guides/index.mdx @@ -59,6 +59,7 @@ Connect DevGuard with your development platforms: ### Dependency Proxy - [Dependency Proxy Overview](/how-to-guides/dependency-proxy) — Use the dependency proxy to intercept and scan packages. +- [Package Rules](/how-to-guides/dependency-proxy/package-rules) — Allow or deny packages with CEL rules, e.g. a minimum release age. - [Setup npm Proxy](/how-to-guides/dependency-proxy/setup-npm-proxy) — Configure the npm dependency proxy. - [Setup PyPI Proxy](/how-to-guides/dependency-proxy/setup-pypi-proxy) — Configure the PyPI dependency proxy. - [Setup Go Proxy](/how-to-guides/dependency-proxy/setup-go-proxy) — Configure the Go dependency proxy.