

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# 根据版本年限对软件包进行门禁
<a name="package-version-age-gating"></a>

**重要**  
版本年龄限制无法抵御在已过隔离期的现有版本中发现的漏洞。 CodeArtifact 与 Amazon Inspector 一起使用，对已建立的软件包进行漏洞扫描。

将软件包版本发布到公共注册表时，注册表会记录发布时间戳。 CodeArtifact 当它缓存来自外部连接的软件包时，会保留这些原始的上游时间戳。您可以在 CI/CD 管道中读取这些时间戳，并拒绝最近发布的版本。

大多数恶意软件包都会在发布后的数小时内被检测并删除。3 到 7 天的隔离期可以阻止大多数供应链攻击，包括依赖混乱、维护者账户接管和域名仿冒等。

**注意**  
本主题中的示例查询 CodeArtifact 存储库 URL，该网址与您的包管理器用于下载软件包的端点相同。所有请求都需要身份验证。在`Authorization: Bearer`标题中传递有效的 CodeArtifact 授权令牌。要获取令牌，请运行`aws codeartifact get-authorization-token`（有关使用`--domain`和的完整调用，请参阅本主题后面的 CI/CD 示例`--domain-owner`）。  
请勿使用返回的`publishedTime`字段`aws codeartifact describe-package-version`进行年龄限制。此字段是在摄取时填充的。对于未捕获原始上游发布时间的记录（包括在引入每工件时间戳捕获之前摄取的包以及某些摄取路径），它会静默地回退到记录的上次更新时间。API 响应并未指明您正在读取两个值中的哪一个，因此您不能依赖它作为上游发布日期。请改用本主题中描述的每种格式的字段。

每种格式的详细信息不同，但是每种格式的门控模式都是一样的：读取每个已解析版本的上游发布时间戳，将其与隔离截止时间（当前时间减去您选择的窗口）进行比较，如果任何版本比截止时间更新，则构建失败。如果您使用多种包裹格式，则在格式特定的字段中应用相同的模式，如下所示。

## npm
<a name="package-version-age-gating-npm"></a>

CodeArtifact 返回 npm packument 字段中每个版本的原始 npmjs.org 发布时间戳。`time`许多软件包管理器和安全工具都是以本地方式读取此字段。其中包括 Yarn 4 `npmMinimalAgeGate` 选项、Renovate `minimumReleaseAge` 设置和 pnpm `minimumReleaseAge` 设置，所有这些设置都 CodeArtifact 无需修改即可使用。

**请求:**

```
curl -H "Authorization: Bearer $CODEARTIFACT_AUTH_TOKEN" \
  https://my-domain-111122223333.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/axios
```

**回应（缩写）：**

```
{
  "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"
  }
}
```

**关键字段：**`time["<version>"]`— 该值是来自 npmjs.org 的原始发布日期。例如，`time["1.7.2"]`返回`2024-05-21T16:58:04.163Z`，这是 npmjs.org 在该版本发布时记录的确切时间戳。要按年龄进行门禁，请将此值与当前时间减去隔离时段的值进行比较。

## Maven
<a name="package-version-age-gating-maven"></a>

对于 Maven 工件，当您下载文件（JAR、POM 或其他工件）时，会在 `Last-Modified` HTTP 响应标头中 CodeArtifact 返回原始的 Maven Central 发布时间戳。

**请求:**

```
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
```

**响应标头：**

```
HTTP/2 200
content-type: application/xml
Last-Modified: Fri, 26 Feb 2021 20:40:52 GMT
content-length: 22672
```

**关键字段：**`Last-Modified`标题 — 这是 Maven Central 的原始发布日期。将其与您的隔离截止时间进行比较，以确定该工件是否太新。

**注意**  
里面的`<lastUpdated>`字段`maven-metadata.xml`反映的是 CodeArtifact 上次刷新缓存的时间，而不是上游发布日期。请勿使用`maven-metadata.xml`时间戳进行年龄限制。

## NuGet
<a name="package-version-age-gating-nuget"></a>

对于 NuGet 软件包，在 CodeArtifact V3 注册响应`published`字段中返回原始的 nuget.org 发布时间戳。

**请求:**

```
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
```

**回应（缩写，显示一个版本）：**

```
{
  "items": [{
    "items": [{
      "catalogEntry": {
        "id": "Newtonsoft.Json",
        "version": "13.0.3",
        "published": "2023-03-08T07:42:54.647+00:00",
        "listed": true
      }
    }]
  }]
}
```

**关键字段：**`catalogEntry.published`— 这是来自 nuget.org 的原始发布日期。例如，`"2023-03-08T07:42:54.647+00:00"`均值 Newtonsoft.Json 13.0.3 已于 2023 年 3 月 8 日发布。

## 货物（生锈）
<a name="package-version-age-gating-cargo"></a>

对于从 crates.io 中提取的货箱，在 crates.io V1 API 响应中 CodeArtifact 保留原始发布时间戳。`versions`查询 V1 API 端点以获取用于读取每个`created_at`版本的时间戳的 crate。

**请求:**

```
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
```

**关键字段：**V1 `versions` 响应中的每个版本`created_at`的时间戳是 crates.io 的原始发布日期。

**注意**  
Cargo 使用稀疏索引协议 (`<prefix>/<crate>`) 来解析正常的依赖关系。稀疏索引会`features`为每个版本返回`vers``cksum``yanked`、、和，但它不包含发布时间戳。要控制 Cargo 对年龄的依赖性，请在管道的单独步骤中查询上面显示的 V1 API。标准`cargo build`和`cargo update`命令不调用 V1 API。

## PyPI
<a name="package-version-age-gating-pypi"></a>

对于 Python 软件包， CodeArtifact 支持 PEP 691（JSON Simple API）和 PEP 700，其中包括每个分发`upload-time`文件的字段。

**请求:**

```
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/
```

**回应（缩写）：**

```
{
  "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"
  }]
}
```

**关键字段：**`upload-time`— 这是PyPI的原始发布日期。诸如此类的工具本身就`uv`支持这一点：

```
# Only install versions published before a specific date
uv pip install --exclude-newer 2026-05-01T00:00:00Z -r requirements.txt
```

**注意**  
PEP 691 JSON API 需要标头。`Accept: application/vnd.pypi.simple.v1+json`默认的 HTML 简单 API 不包含时间戳。  
对于在引入按工件上传时间捕获之前提取的分发文件（旧记录），中的条目中可能没有该`upload-time`字段。`files[]`您的门禁逻辑应`upload-time`将缺失视为无法验证的版本，并根据您的政策决定是允许还是屏蔽该版本。

## 示例：对 npm 包进行门控 CI/CD
<a name="package-version-age-gating-example"></a>

以下 Python 脚本读取`package-lock.json`文件，查询 CodeArtifact 每个依赖项的发布时间戳，如果在隔离窗口内发布了任何版本，则以非零状态退出。

```
#!/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 集成：**

```
# 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
```

**包裹被封锁时的输出示例：**

```
BLOCKED: 1 package(s) newer than 72 hours:
  evil-package@1.0.0 (age: 2h, published 2026-06-03T08:30:00+00:00)
```

**所有包都通过时的输出示例：**

```
PASSED: all 847 packages older than 72 hours.
```

## 攻击得到缓解
<a name="package-version-age-gating-attacks"></a>


| 攻击类型 | 工作原理 | 年龄门控有何帮助 | 
| --- | --- | --- | 
| 依赖关系混乱 | 攻击者将与内部软件包同名的高版本软件包发布到公共注册表。 | 恶意版本是全新的。被隔离窗口阻止。 | 
| 账户接管 | 攻击者泄露维护者的凭据并发布恶意更新（例如，事件流）。 | 新版本已保存。社区会在隔离期内对其进行检测并恢复。 | 
| 域名仿名 | 攻击者发布了一个名称与流行软件包相似的软件包（例如，lodashs而不是lodash）。 | typosquat 软件包的所有版本都是新的。每个版本都被屏蔽。 | 
| Star-jacking | 攻击者将恶意软件注入他们控制的合法软件包的补丁版本中。 | 恶意补丁是新发布的版本。在超过隔离期限之前一直处于封锁状态。 | 

## 支持的格式摘要
<a name="package-version-age-gating-summary"></a>


| Format | 在哪里阅读发布时间戳 | 关键字段 | Status | 
| --- | --- | --- | --- | 
| npm | 包裹回复 () GET /<package> | time["<version>"] | 可用 | 
| Maven | 下载工件时的 HTTP 响应标头 | Last-Modified | 可用 | 
| NuGet | V3 注册索引 | catalogEntry.published | 可用 | 
| 货物 | V1 API 版本响应 | created\_at | 可用 | 
| PyPI | PEP 691 JSON 简单 API | upload-time | 可用 | 