Gating packages on version age
Important
Version age gating does not protect against vulnerabilities discovered in existing versions that have already passed the quarantine window. Use CodeArtifact with Amazon Inspector for vulnerability scanning of established packages.
When a package version is published to a public registry, the registry records the publish timestamp. CodeArtifact preserves these original upstream timestamps when it caches packages from external connections. You can read these timestamps in your CI/CD pipeline and reject versions that were published too recently.
Most malicious packages are detected and removed within hours of publication. A quarantine window of 3 to 7 days blocks the majority of supply chain attacks, including dependency confusion, maintainer account takeover, and typosquatting.
Note
The examples in this topic query the CodeArtifact repository URL, which is the same
endpoint your package manager uses to download packages. All requests require
authentication. Pass a valid CodeArtifact authorization token in the
Authorization: Bearer header. To get a token, run
aws codeartifact get-authorization-token (see the CI/CD example later in
this topic for the full invocation with --domain and
--domain-owner).
Do not use the publishedTime field returned by
aws codeartifact describe-package-version for age gating. This field is
populated at ingestion time. For records where the original upstream publish time was
not captured (including packages ingested before per-artifact timestamp capture was
introduced, and certain ingestion paths), it silently falls back to the record's
last-updated time. The API response does not indicate which of the two values you are
reading, so you cannot rely on it as the upstream publish date. Use the per-format
fields described in this topic instead.
The per-format details differ, but the gating pattern is the same for every format: read the upstream publish timestamp for each resolved version, compare it against your quarantine cutoff (the current time minus your chosen window), and fail the build if any version is newer than the cutoff. If you use more than one package format, you apply the same pattern with the format-specific field shown below.
npm
CodeArtifact returns the original npmjs.org publish timestamp for every version in the
npm packument time field. Many package managers and security tools read
this field natively. These include the Yarn 4 npmMinimalAgeGate option,
the Renovate minimumReleaseAge setting, and the pnpm
minimumReleaseAge setting, all of which work with CodeArtifact without
modification.
Request:
curl -H "Authorization: Bearer $CODEARTIFACT_AUTH_TOKEN" \ https://my-domain-111122223333.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/axios
Response (abbreviated):
{ "name": "axios", "time": { "1.6.0": "2023-10-26T21:15:55.685Z", "1.6.5": "2024-01-05T19:52:15.051Z", "1.7.0": "2024-05-19T20:25:03.615Z", "1.7.1": "2024-05-20T13:32:52.757Z", "1.7.2": "2024-05-21T16:58:04.163Z" } }
Key field: time["<version>"] —
the value is the original publish date from npmjs.org. For example,
time["1.7.2"] returns 2024-05-21T16:58:04.163Z, which is
the exact timestamp recorded by npmjs.org when that version was published.
To gate on age, compare this value to the current time minus your quarantine window.
Maven
For Maven artifacts, CodeArtifact returns the original Maven Central publish timestamp in
the Last-Modified HTTP response header when you download a file (JAR, POM,
or other artifact).
Request:
curl -I -H "Authorization: Bearer $CODEARTIFACT_AUTH_TOKEN" \ https://my-domain-111122223333.d.codeartifact.us-east-1.amazonaws.com/maven/my-repo/\ org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.pom
Response headers:
HTTP/2 200 content-type: application/xml Last-Modified: Fri, 26 Feb 2021 20:40:52 GMT content-length: 22672
Key field: Last-Modified header —
this is the original publish date from Maven Central. Compare it to your quarantine
cutoff to determine if the artifact is too new.
Note
The <lastUpdated> field inside maven-metadata.xml
reflects when CodeArtifact last refreshed its cache, not the upstream publish date. Do not
use maven-metadata.xml timestamps for age gating.
NuGet
For NuGet packages, CodeArtifact returns the original nuget.org publish timestamp in the
published field of the V3 registration response.
Request:
curl -H "Authorization: Bearer $CODEARTIFACT_AUTH_TOKEN" \ https://my-domain-111122223333.d.codeartifact.us-east-1.amazonaws.com/nuget/my-repo/\ v3/registration4/newtonsoft.json/index.json
Response (abbreviated, showing one version):
{ "items": [{ "items": [{ "catalogEntry": { "id": "Newtonsoft.Json", "version": "13.0.3", "published": "2023-03-08T07:42:54.647+00:00", "listed": true } }] }] }
Key field: catalogEntry.published —
this is the original publish date from nuget.org. For example,
"2023-03-08T07:42:54.647+00:00" means Newtonsoft.Json 13.0.3 was
published on March 8, 2023.
Cargo (Rust)
For Cargo crates pulled from crates.io, CodeArtifact preserves the original publish
timestamp in the crates.io V1 API versions response. Query the V1 API
endpoint for a crate to read the per-version created_at timestamp.
Request:
curl -H "Authorization: Bearer $CODEARTIFACT_AUTH_TOKEN" \ https://my-domain-111122223333.d.codeartifact.us-east-1.amazonaws.com/cargo/my-repo/\ api/v1/crates/serde/versions
Key field: The per-version created_at
timestamp in the V1 versions response is the original crates.io publish
date.
Note
Cargo uses the sparse index protocol
(<prefix>/<crate>) for normal dependency resolution. The
sparse index returns vers, cksum, yanked, and
features for each version, but it does not include a publish timestamp.
To gate Cargo dependencies on age, query the V1 API shown above in a separate step in
your pipeline. The standard cargo build and cargo update
commands do not call the V1 API.
PyPI
For Python packages, CodeArtifact supports PEP 691 (JSON Simple API) and PEP 700, which
include the upload-time field for each distribution file.
Request:
curl -H "Authorization: Bearer $CODEARTIFACT_AUTH_TOKEN" \ -H "Accept: application/vnd.pypi.simple.v1+json" \ https://my-domain-111122223333.d.codeartifact.us-east-1.amazonaws.com/pypi/my-repo/simple/requests/
Response (abbreviated):
{ "files": [{ "filename": "requests-2.31.0-py3-none-any.whl", "upload-time": "2023-05-22T15:12:42.313790Z", "size": 62574, "url": "../../packages/requests/2.31.0/requests-2.31.0-py3-none-any.whl" }] }
Key field: upload-time —
this is the original publish date from PyPI. Tools such as uv support
this natively:
# Only install versions published before a specific date uv pip install --exclude-newer 2026-05-01T00:00:00Z -r requirements.txt
Note
The PEP 691 JSON API requires the Accept: application/vnd.pypi.simple.v1+json
header. The default HTML Simple API does not include timestamps.
For distribution files ingested before per-artifact upload-time capture was
introduced (legacy records), the upload-time field may be absent from
entries in files[]. Your gating logic should treat a missing
upload-time as a version it cannot verify, and decide whether to allow
or block it according to your policy.
Example: Gating npm packages in CI/CD
The following Python script reads a package-lock.json file, queries
CodeArtifact for the publish timestamp of each dependency, and exits with a non-zero status
if any version was published within the quarantine window.
#!/usr/bin/env python3 """ gate_package_age.py - Block npm packages published too recently. Reads package-lock.json, queries CodeArtifact for each dependency's publish timestamp, and rejects versions newer than QUARANTINE_HOURS. Environment variables: CODEARTIFACT_AUTH_TOKEN - from `aws codeartifact get-authorization-token` CODEARTIFACT_NPM_ENDPOINT - from `aws codeartifact get-repository-endpoint` QUARANTINE_HOURS - minimum version age in hours (default: 72) """ import json, os, sys, urllib.request from datetime import datetime, timezone, timedelta def get_publish_time(endpoint, token, package, version): """Fetch the publish timestamp for a specific version from CodeArtifact.""" url = f"{endpoint.rstrip('/')}/{package}" req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"}) data = json.loads(urllib.request.urlopen(req, timeout=30).read()) time_str = data.get("time", {}).get(version) if not time_str: return None return datetime.fromisoformat(time_str.replace("Z", "+00:00")) token = os.environ["CODEARTIFACT_AUTH_TOKEN"] endpoint = os.environ["CODEARTIFACT_NPM_ENDPOINT"] quarantine_hours = int(os.environ.get("QUARANTINE_HOURS", "72")) cutoff = datetime.now(timezone.utc) - timedelta(hours=quarantine_hours) with open("package-lock.json") as f: lockfile = json.load(f) blocked = [] checked = 0 for path, info in lockfile.get("packages", {}).items(): if not path.startswith("node_modules/"): continue name = path.replace("node_modules/", "", 1) version = info.get("version") if not name or not version: continue checked += 1 pub_time = get_publish_time(endpoint, token, name, version) if pub_time and pub_time > cutoff: age_h = (datetime.now(timezone.utc) - pub_time).total_seconds() / 3600 blocked.append(f" {name}@{version} (age: {age_h:.0f}h, published {pub_time.isoformat()})") if blocked: print(f"BLOCKED: {len(blocked)} package(s) newer than {quarantine_hours} hours:") print("\n".join(blocked)) sys.exit(1) print(f"PASSED: all {checked} packages older than {quarantine_hours} hours.")
CI/CD integration:
# Step 1: Authenticate export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \ --domain my-domain --domain-owner 111122223333 \ --query authorizationToken --output text) export CODEARTIFACT_NPM_ENDPOINT=$(aws codeartifact get-repository-endpoint \ --domain my-domain --domain-owner 111122223333 \ --repository my-repo --format npm \ --query repositoryEndpoint --output text) # Step 2: Set quarantine window (72 hours = 3 days) export QUARANTINE_HOURS=72 # Step 3: Generate lockfile without installing npm install --package-lock-only # Step 4: Gate on age — fails if any dependency is too new python3 gate_package_age.py # Step 5: If gate passes, install npm ci
Example output when a package is blocked:
BLOCKED: 1 package(s) newer than 72 hours: evil-package@1.0.0 (age: 2h, published 2026-06-03T08:30:00+00:00)
Example output when all packages pass:
PASSED: all 847 packages older than 72 hours.
Attacks mitigated
| Attack type | How it works | How age gating helps |
|---|---|---|
| Dependency confusion | Attacker publishes a high-version package with the same name as an internal package to a public registry. | The malicious version is brand new. Blocked by quarantine window. |
| Account takeover | Attacker compromises a maintainer's credentials and publishes a malicious update (for example, event-stream). | The new version is held. Community detects and reverts it within the quarantine period. |
| Typosquatting | Attacker publishes a package with a name similar to a popular
package (for example, lodashs instead of lodash). |
All versions of the typosquat package are new. Every version is blocked. |
| Star-jacking | Attacker injects malware into a patch release of a legitimate package they control. | The malicious patch is a newly published version. Blocked until it ages past the quarantine window. |
Supported formats summary
| Format | Where to read publish timestamp | Key field | Status |
|---|---|---|---|
| npm | Packument response (GET /<package>) |
time["<version>"] |
Available |
| Maven | HTTP response header on artifact download | Last-Modified |
Available |
| NuGet | V3 registration index | catalogEntry.published |
Available |
| Cargo | V1 API versions response | created_at |
Available |
| PyPI | PEP 691 JSON Simple API | upload-time |
Available |