openapi: 3.1.0
info:
  title: VibeGuard Public API
  version: 1.1.0
  description: |
    VibeGuard 为开发者、安全团队和 Agent 工具提供供应链威胁情报公开 API。
    接口覆盖安全文章检索、来源统计、漏洞包检查、结构化公告查询、CVE 富集信息和同步状态。
    公开接口无需认证；Admin 与 MCP 传输端点不在本文件描述范围内。
  contact:
    name: VibeGuard
    url: https://github.com/27Aaron/VibeGuard

externalDocs:
  description: VibeGuard API 页面
  url: /zh/api

servers:
  - url: /
    description: 当前站点根地址。部署到线上时保持相对路径，便于反向代理和多域名复用。

tags:
  - name: Public
    description: 站点公开阅读流的概览和来源接口。
  - name: Articles
    description: 安全文章列表和详情接口，只返回 ready 状态文章。
  - name: Security
    description: 本地安全镜像查询接口，面向包坐标、公告、CVE 和同步状态。

# 公开内容接口：适合网页、RSS/MCP 工具和轻量客户端读取。
paths:
  /api/overview:
    get:
      operationId: getPublicOverview
      summary: 公开站点概览
      description: 返回当前公开阅读流的文章数量和来源数量，适合首页、健康检查或监控面板快速展示。
      tags: [Public]
      responses:
        "200":
          description: 文章与来源数量概览。
          content:
            application/json:
              schema:
                type: object
                required: [meta, overview]
                properties:
                  meta:
                    $ref: "#/components/schemas/PublicMeta"
                  overview:
                    $ref: "#/components/schemas/PublicOverview"
              examples:
                currentSite:
                  summary: 当前站点概览
                  value:
                    meta:
                      generatedAt: "2026-05-24T12:24:04+08:00"
                      generatedAtDisplay: "2026-05-24 12:24"
                    overview:
                      articleCount: 101
                      sourceCount: 1
      x-codeSamples:
        - lang: cURL
          label: 获取站点概览
          source: |
            curl -sS 'http://127.0.0.1:3000/api/overview'

  /api/sources:
    get:
      operationId: listPublicSources
      summary: 公开来源列表
      description: 返回每个公开内容来源及其已发布文章数量。可用于来源筛选器、数据源覆盖率展示或 Agent 选择查询范围。
      tags: [Public]
      responses:
        "200":
          description: 已发布文章来源及数量。
          content:
            application/json:
              schema:
                type: object
                required: [meta, items]
                properties:
                  meta:
                    allOf:
                      - $ref: "#/components/schemas/PublicMeta"
                      - type: object
                        properties:
                          count:
                            type: integer
                            description: 来源数量。
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/SourceSummary"
              examples:
                sourceList:
                  value:
                    meta:
                      count: 1
                      generatedAt: "2026-05-24T12:24:04+08:00"
                      generatedAtDisplay: "2026-05-24 12:24"
                    items:
                      - sourceName: GitHub Advisory
                        count: 12
      x-codeSamples:
        - lang: cURL
          label: 获取来源列表
          source: |
            curl -sS 'http://127.0.0.1:3000/api/sources'

  /api/articles:
    get:
      operationId: listArticles
      summary: 查询文章列表
      description: |
        查询公开安全文章。接口会强制只返回 ready 状态，调用方即使传入 status 也会被忽略，避免泄露后台处理中内容。
        返回标题和摘要会根据 lang 参数选择中文或英文；详情正文请继续调用 /api/articles/{articleId}。
      tags: [Articles]
      parameters:
        - name: lang
          in: query
          schema: { type: string, enum: [zh, en], default: zh }
          description: 返回内容语言。zh 返回中文标题/摘要；en 返回英文标题/摘要。
          example: zh
        - name: q
          in: query
          schema: { type: string }
          description: 搜索关键词，匹配标题、摘要、来源与标签。支持中英文。
          example: cve
        - name: tag
          in: query
          schema: { type: string }
          description: 按标签筛选。标签通常由规则或 LLM 生成，例如 cve、npm、supply-chain。
          example: npm
        - name: source
          in: query
          schema: { type: string }
          description: 按来源名称筛选，需要与 /api/sources 返回的 sourceName 对齐。
          example: GitHub Advisory
        - name: ecosystem
          in: query
          schema:
            {
              type: string,
              enum:
                [
                  npm,
                  pypi,
                  maven,
                  go,
                  crates-io,
                  github-actions,
                  docker,
                  multi,
                ],
            }
          description: 按文章涉及的主要包生态筛选。
          example: npm
        - name: riskCategory
          in: query
          schema:
            {
              type: string,
              enum:
                [
                  vulnerability,
                  exploit-activity,
                  malicious-package,
                  supply-chain-attack,
                  dependency-risk,
                ],
            }
          description: 按风险类别筛选。
          example: malicious-package
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
          description: 每页数量。小于 1 或非数字时回退到默认值；超过 100 会被截断为 100。
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
          description: 页码，从 1 开始。
      responses:
        "200":
          description: 文章列表。
          content:
            application/json:
              schema:
                type: object
                required: [meta, items]
                properties:
                  meta:
                    $ref: "#/components/schemas/ArticleListMeta"
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Article"
              examples:
                keywordSearch:
                  summary: 搜索 CVE 相关文章
                  value:
                    meta:
                      lang: zh
                      status: ready
                      query: cve
                      limit: 2
                      count: 2
                      page: 1
                      pageSize: 2
                      totalCount: 8
                      totalPages: 4
                    items: []
      x-codeSamples:
        - lang: cURL
          label: 搜索文章
          source: |
            curl -sS 'http://127.0.0.1:3000/api/articles?q=cve&limit=2&lang=zh'
        - lang: cURL
          label: 按标签筛选
          source: |
            curl -sS 'http://127.0.0.1:3000/api/articles?tag=npm&limit=5'

  /api/articles/{articleId}:
    get:
      operationId: getArticleById
      summary: 获取文章详情
      description: 根据文章 ID 获取完整详情，包含 Markdown 正文、规范链接、元数据、标签和发布时间。建议先从 /api/articles 列表中取得 articleId。
      tags: [Articles]
      parameters:
        - name: articleId
          in: path
          required: true
          schema: { type: string, format: uuid }
          description: UUID 格式文章 ID。
          example: 8b761e1e-ba00-4bbd-af4b-ee4c605a5531
        - name: lang
          in: query
          schema: { type: string, enum: [zh, en], default: zh }
          description: 返回正文语言。没有对应语言内容时，服务端会按内容可用性回退。
      responses:
        "200":
          description: 文章详情。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArticleDetail"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: 获取文章详情
          source: |
            curl -sS 'http://127.0.0.1:3000/api/articles/{articleId}?lang=zh'

  /api/security/check/overview:
    get:
      operationId: getSecurityCheckOverview
      summary: 漏洞统计概览
      description: 返回本地安全镜像中各生态系统的受影响包记录数。该接口用于判断镜像大致覆盖范围，不代表唯一漏洞数量。
      tags: [Security]
      responses:
        "200":
          description: 各生态系统受影响包数量。
          content:
            application/json:
              schema:
                type: object
                required: [totals]
                properties:
                  totals:
                    $ref: "#/components/schemas/SecurityEcosystemTotals"
              examples:
                ecosystemTotals:
                  value:
                    totals:
                      npm: 20337
                      pypi: 21115
                      go: 9143
                      crates-io: 3007
      x-codeSamples:
        - lang: cURL
          label: 获取安全数据概览
          source: |
            curl -sS 'http://127.0.0.1:3000/api/security/check/overview'

  /api/security/check/packages:
    post:
      operationId: checkSecurityPackages
      summary: 批量检查包漏洞
      description: |
        批量检查依赖包是否存在已知安全漏洞或其他安全风险。请求体只应包含规范化包坐标，不要上传完整项目、lockfile 或源码。
        单次最多 100 个包。传 version 时会尝试做版本范围判断；不传 version 时返回该包所有已知命中。
      tags: [Security]
      requestBody:
        required: true
        description: packages 数组必须包含 1-100 个包坐标。
        content:
          application/json:
            schema:
              type: object
              required: [packages]
              properties:
                packages:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    $ref: "#/components/schemas/PackageCoordinate"
            examples:
              mixedEcosystems:
                summary: 混合生态批量检查
                value:
                  packages:
                    - ecosystem: npm
                      name: lodash
                      version: 4.17.20
                    - ecosystem: pypi
                      name: requests
                      version: 2.19.0
      responses:
        "200":
          description: 检查结果。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PackageCheckResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/TooManyRequests"
      x-codeSamples:
        - lang: cURL
          label: 批量检查依赖包
          source: |
            curl -sS 'http://127.0.0.1:3000/api/security/check/packages' \
              -H 'content-type: application/json' \
              -d '{"packages":[{"ecosystem":"npm","name":"lodash","version":"4.17.20"}]}'

  /api/security/advisories:
    get:
      operationId: listSecurityAdvisories
      summary: 查询安全公告
      description: 查询结构化安全公告，可按包、CVE、风险类型、CISA KEV、CVSS、EPSS、撤回状态和更新时间筛选。
      tags: [Security]
      parameters:
        - name: q
          in: query
          schema: { type: string }
          description: 搜索 GHSA、MAL、摘要、详情、CVE、related 或 upstream ID。
          example: axios
        - name: ecosystem
          in: query
          schema: { type: string, enum: [npm, pypi, go, crates-io] }
          description: 包生态。与 package 一起使用时可定位某个包相关公告。
        - name: package
          in: query
          schema: { type: string }
          description: 包名。PyPI 会做大小写与分隔符规范化；npm/crates.io 会转小写；Go 保留路径大小写。
          example: axios
        - name: cve
          in: query
          schema: { type: string, pattern: '^CVE-\d{4}-\d{4,}$' }
          description: CVE 编号，服务端会规范为大写。
          example: CVE-2026-25639
        - name: riskType
          in: query
          schema:
            { type: string, enum: [unknown, vulnerability, malicious-package] }
          description: 风险类型。
        - name: kev
          in: query
          schema: { type: boolean }
          description: true 只返回 CISA KEV 已列入公告；false 只返回未列入公告。
        - name: withdrawn
          in: query
          schema: { type: boolean }
          description: 是否筛选已撤回公告。
        - name: cvssMin
          in: query
          schema: { type: number, minimum: 0, maximum: 10 }
          description: 只返回最高 CVSS 分数不低于该值的公告。
        - name: epssMin
          in: query
          schema: { type: number, minimum: 0, maximum: 1 }
          description: 只返回 EPSS percentile 不低于该值的公告。
        - name: updatedAfter
          in: query
          schema: { type: string, format: date-time }
          description: 只返回在该时间之后更新、发布或撤回的公告。
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
      responses:
        "200":
          description: 安全公告列表。
          content:
            application/json:
              schema:
                type: object
                required: [meta, items]
                properties:
                  meta:
                    $ref: "#/components/schemas/SecurityAdvisoryListMeta"
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/SecurityAdvisory"
      x-codeSamples:
        - lang: cURL
          label: 查询 axios 公告
          source: |
            curl -sS 'http://127.0.0.1:3000/api/security/advisories?ecosystem=npm&package=axios&limit=3'
        - lang: cURL
          label: 查询 CISA KEV 公告
          source: |
            curl -sS 'http://127.0.0.1:3000/api/security/advisories?kev=true&limit=3'

  /api/security/advisories/{advisoryId}:
    get:
      operationId: getSecurityAdvisory
      summary: 获取单条安全公告
      description: 根据 OSV/GHSA/MAL external id 获取单条公告详情，包含影响包、范围、修复版本、关联 ID 和 CVE 富集信息。
      tags: [Security]
      parameters:
        - name: advisoryId
          in: path
          required: true
          schema: { type: string }
          description: OSV/GHSA/MAL external id，例如 GHSA-43fc-jf86-j433。
          example: GHSA-43fc-jf86-j433
      responses:
        "200":
          description: 安全公告详情。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecurityAdvisory"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: 获取公告详情
          source: |
            curl -sS 'http://127.0.0.1:3000/api/security/advisories/GHSA-43fc-jf86-j433'

  /api/security/packages/{ecosystem}/{packageName}:
    get:
      operationId: getSecurityPackageProfile
      summary: 获取包风险画像
      description: 获取单个包的风险画像，返回命中数量、确认影响数量、最高风险、最近更新、修复版本建议和完整 findings。
      tags: [Security]
      parameters:
        - name: ecosystem
          in: path
          required: true
          schema: { type: string, enum: [npm, pypi, go, crates-io] }
          description: 包生态。
        - name: packageName
          in: path
          required: true
          schema: { type: string }
          description: 包名或模块路径。npm scope 和 Go module 路径可使用多段路径。
          example: axios
        - name: version
          in: query
          schema: { type: string, nullable: true }
          description: 可选版本；传入后按版本范围判断是否确认命中。
          example: 1.0.0
      responses:
        "200":
          description: 包风险画像。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PackageProfile"
        "400":
          $ref: "#/components/responses/BadRequest"
      x-codeSamples:
        - lang: cURL
          label: 获取 npm 包风险画像
          source: |
            curl -sS 'http://127.0.0.1:3000/api/security/packages/npm/axios?version=1.0.0'
        - lang: cURL
          label: 获取 Go module 风险画像
          source: |
            curl -sS 'http://127.0.0.1:3000/api/security/packages/go/golang.org/x/net'

  /api/security/cves/{cveId}:
    get:
      operationId: getSecurityCve
      summary: 获取 CVE 富集详情
      description: 获取 CVE 富集详情，包含 CVSS、CWE、EPSS、CISA KEV、NVD 时间和与该 CVE 关联的安全公告。
      tags: [Security]
      parameters:
        - name: cveId
          in: path
          required: true
          schema: { type: string, pattern: '^CVE-\d{4}-\d{4,}$' }
          description: CVE 编号。
          example: CVE-2026-25639
      responses:
        "200":
          description: CVE 富集数据与相关公告。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CveDetail"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: cURL
          label: 获取 CVE 详情
          source: |
            curl -sS 'http://127.0.0.1:3000/api/security/cves/CVE-2026-25639'

  /api/security/sync/status:
    get:
      operationId: getSecuritySyncStatus
      summary: 安全数据同步状态
      description: 查看 OSV、NVD、FIRST EPSS、CISA KEV 等安全数据源的同步状态、数据新鲜度、导入数量和最近错误。
      tags: [Security]
      responses:
        "200":
          description: OSV/NVD/EPSS/KEV 等来源同步状态。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecuritySyncStatusResponse"
      x-codeSamples:
        - lang: cURL
          label: 查看同步状态
          source: |
            curl -sS 'http://127.0.0.1:3000/api/security/sync/status'

components:
  responses:
    BadRequest:
      description: 请求参数或请求体不符合接口约束。
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            invalidPackages:
              summary: packages 为空
              value:
                ok: false
                message: packages must include at least one package.
    NotFound:
      description: 指定资源不存在，或文章尚未公开发布。
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error:
                type: string
                description: 面向调用方的错误说明。
          examples:
            missingArticle:
              value:
                error: 未找到对应文章。
    TooManyRequests:
      description: 请求过多。包检查接口当前有基础限流，调用方应分批和重试。
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            rateLimited:
              value:
                ok: false
                message: Too many requests. Please try again later.

  schemas:
    ErrorResponse:
      type: object
      description: 标准错误响应。公开接口不会返回堆栈或内部数据库错误。
      required: [ok, message]
      properties:
        ok:
          type: boolean
          example: false
        message:
          type: string
          example: packages must include at least one package.

    PublicMeta:
      type: object
      description: 公共接口的生成时间。display 字段固定使用 Asia/Shanghai 展示格式。
      properties:
        generatedAt:
          type: string
          format: date-time
          example: "2026-05-24T12:24:04+08:00"
        generatedAtDisplay:
          type: string
          example: "2026-05-24 12:24"

    PublicOverview:
      type: object
      description: 首页和公开浏览流的基础计数。
      properties:
        articleCount:
          type: integer
          description: 当前公开可读文章数量，只包含 ready 状态。
          example: 101
        sourceCount:
          type: integer
          description: 当前公开来源数量。
          example: 1

    SourceSummary:
      type: object
      description: 一个公开来源及其已发布文章数量。
      properties:
        sourceName:
          type: string
          example: GitHub Advisory
        count:
          type: integer
          example: 12

    ArticleListMeta:
      type: object
      description: 文章列表分页和筛选回显。status 对公开 API 固定为 ready。
      properties:
        lang:
          type: string
          enum: [zh, en]
          description: 本次返回内容使用的语言。
        status:
          type: string
          description: 公开 API 强制只返回 ready 文章。
          example: ready
        source:
          type: string
          nullable: true
          description: 来源筛选回显；未传时为 null。
        query:
          type: string
          nullable: true
          description: 关键词筛选回显；未传时为 null。
        ecosystem:
          type: string
          nullable: true
          description: 生态系统筛选回显。
        riskCategory:
          type: string
          nullable: true
          description: 风险类别筛选回显。
        tag:
          type: string
          nullable: true
          description: 标签筛选回显。
        limit:
          type: integer
          description: 请求页大小，最大 100。
          example: 20
        count:
          type: integer
          description: 当前页实际返回数量。
        page:
          type: integer
          description: 当前页码，从 1 开始。
        pageSize:
          type: integer
          description: 与 limit 相同，便于客户端统一读取。
        totalCount:
          type: integer
          description: 当前筛选条件下的总数量。
        totalPages:
          type: integer
          description: 当前筛选条件下的总页数。

    Article:
      type: object
      description: 公开文章列表项。正文 content 只在详情接口返回。
      properties:
        id:
          type: string
          format: uuid
          description: 文章唯一 ID，可继续用于 /api/articles/{articleId}。
        title:
          type: string
          description: 按 lang 返回的标题。
        summary:
          type: string
          description: 按 lang 返回的摘要，可能是 Markdown。
        url:
          type: string
          format: uri
          nullable: true
          description: 原始来源 URL。
        sourceName:
          type: string
          description: RSS 或内容来源名称。
        ecosystem:
          type: string
          description: 文章涉及的主要生态系统。
        riskCategory:
          type: string
          description: 文章风险类别。
        tags:
          type: array
          description: LLM 或规则生成的标签，适合继续作为 tag 筛选。
          items:
            type: string
        status:
          type: string
          example: ready
        publishedAt:
          type: string
          format: date-time
          description: 发布时间，带 Asia/Shanghai offset。
        publishedAtDisplay:
          type: string
          description: 面向界面展示的北京时间。
        updatedAt:
          type: string
          format: date-time
        updatedAtDisplay:
          type: string
        locale:
          type: string
          enum: [zh, en]
          description: 实际命中的内容语言。

    ArticleDetail:
      allOf:
        - $ref: "#/components/schemas/Article"
        - type: object
          description: 文章详情比列表项多返回 Markdown 正文和规范链接。
          properties:
            content:
              type: string
              description: Markdown 正文。
            canonicalUrl:
              type: string
              format: uri
              nullable: true
              description: 来源站点提供的 canonical URL。

    SecurityEcosystemTotals:
      type: object
      description: 本地安全镜像中按生态统计的受影响包记录数，不等于唯一漏洞数。
      properties:
        npm:
          type: integer
          example: 20337
        pypi:
          type: integer
          example: 21115
        go:
          type: integer
          example: 9143
        crates-io:
          type: integer
          example: 3007

    PackageCoordinate:
      type: object
      description: 包检查请求只接收规范化包坐标，不接收完整仓库、lockfile 或源码内容。
      required: [ecosystem, name]
      properties:
        ecosystem:
          type: string
          enum: [npm, pypi, go, crates-io]
          description: 包所在生态。公开检查接口目前支持 npm、PyPI、Go、crates.io。
          example: npm
        name:
          type: string
          description: 包名或模块路径。Go module 使用完整路径，npm scope 需包含 @scope/name。
          example: axios
        version:
          type: string
          nullable: true
          description: 可选版本号。传入后 API 会尝试按版本范围判断是否确认受影响；不传则返回该包全部已知命中。
          example: 1.0.0

    PackageCheckMeta:
      type: object
      description: 本次包检查使用的数据源状态。
      properties:
        source:
          type: string
          enum: [local-osv-mirror]
          description: 当前查询使用本地 OSV 镜像。
        lastSyncedAt:
          type: string
          format: date-time
          nullable: true
          description: 最近一次成功同步时间。
        stale:
          type: boolean
          description: 当前镜像是否超过新鲜度阈值。

    PackageCheckResponse:
      type: object
      description: 批量包检查结果。findings 是扁平数组，每条记录对应一个包坐标与一个安全公告的匹配关系。
      properties:
        meta:
          $ref: "#/components/schemas/PackageCheckMeta"
        findings:
          type: array
          items:
            $ref: "#/components/schemas/SecurityFinding"

    PackageProfile:
      type: object
      description: 单个包的风险画像，适合包详情页或 Agent 查询工具直接消费。
      properties:
        package:
          $ref: "#/components/schemas/PackageCoordinate"
        meta:
          $ref: "#/components/schemas/PackageCheckMeta"
        summary:
          type: object
          description: 从 findings 汇总出的包级风险摘要。
          properties:
            totalFindings:
              type: integer
              description: 总命中记录数。
            affectedCount:
              type: integer
              description: 版本判断确认 affected=true 的记录数。
            inconclusiveCount:
              type: integer
              description: 包名命中但版本判断不确定的记录数。
            highestRisk:
              type: object
              nullable: true
              description: 当前包命中的最高风险等级。
              properties:
                level:
                  type: string
                  enum: [critical, high, medium, low, unknown]
                score:
                  type: number
            latestUpdatedAt:
              type: string
              format: date-time
              nullable: true
              description: 相关记录中最近更新时间。
            recommendedFixedVersions:
              type: array
              description: 从公告影响范围汇总出的修复版本候选。
              items:
                type: string
        findings:
          type: array
          items:
            $ref: "#/components/schemas/SecurityFinding"

    SecurityFinding:
      type: object
      description: 一个包坐标与一条安全公告的匹配结果。
      properties:
        affected:
          type: boolean
          description: 当前版本是否确认落在受影响范围内。未传版本或范围无法判断时可能为 false。
        confidence:
          type: string
          enum: [high, medium, low, undetermined]
          description: 版本匹配置信度。
        matchReason:
          type: string
          description: 解释为什么 affected/confidence 得出当前结果。
          enum:
            - explicit_affected_version
            - version_in_ecosystem_range
            - version_outside_ecosystem_range
            - range_present_but_inconclusive
            - package_match_without_version
        matchSummary:
          type: string
          description: 面向人类阅读的匹配摘要。
        package:
          $ref: "#/components/schemas/PackageCoordinate"
        advisory:
          $ref: "#/components/schemas/SecurityAdvisoryBase"
        affectedPackage:
          $ref: "#/components/schemas/PackageImpact"
        cveEnrichments:
          type: array
          description: 与该公告关联的 CVE 富集信息，可能来自 NVD、FIRST EPSS、CISA KEV。
          items:
            $ref: "#/components/schemas/CveEnrichment"
        risk:
          type: object
          nullable: true
          description: 风险评分由静态严重度、EPSS、KEV 和命中置信度综合计算。
          properties:
            level:
              type: string
              enum: [critical, high, medium, low, unknown]
            score:
              type: number
            signals:
              type: array
              items:
                type: string

    SecurityAdvisoryListMeta:
      type: object
      description: 安全公告列表分页和筛选回显。
      properties:
        q: { type: string, description: 关键词筛选回显。 }
        ecosystem:
          { type: string, nullable: true, description: 生态系统筛选回显。 }
        packageName: { type: string, description: 包名筛选回显。 }
        cve: { type: string, nullable: true, description: CVE 筛选回显。 }
        riskType:
          { type: string, nullable: true, description: 风险类型筛选回显。 }
        kev: { type: boolean, nullable: true, description: CISA KEV 筛选回显。 }
        withdrawn:
          { type: boolean, nullable: true, description: 撤回状态筛选回显。 }
        cvssMin:
          { type: number, nullable: true, description: CVSS 最小值筛选回显。 }
        epssMin:
          {
            type: number,
            nullable: true,
            description: EPSS percentile 最小值筛选回显。,
          }
        updatedAfter:
          {
            type: string,
            format: date-time,
            nullable: true,
            description: 更新时间筛选回显。,
          }
        limit: { type: integer }
        page: { type: integer }
        count: { type: integer }
        totalCount: { type: integer }
        totalPages: { type: integer }

    SecurityAdvisoryBase:
      type: object
      description: OSV/GHSA/MAL 公告的基础结构。
      properties:
        id:
          type: string
          description: 外部公告 ID，例如 GHSA 或 MAL ID。
        source:
          type: string
          description: 数据来源。
        sourceUrl:
          type: string
          format: uri
          nullable: true
          description: 上游公告 URL。
        riskType:
          type: string
          enum: [unknown, vulnerability, malicious-package]
        summary:
          type: string
          description: 公告摘要。
        details:
          type: string
          nullable: true
          description: 上游公告详情。
        aliases:
          type: array
          description: 别名，通常包含 CVE。
          items:
            type: string
        related:
          type: array
          description: OSV related IDs。
          items:
            type: string
        upstream:
          type: array
          description: 上游相关 ID。
          items:
            type: string
        severity:
          type: array
          description: OSV severity 原始结构。
          items:
            type: object
        references:
          type: array
          description: 修复、报告、网页等参考链接。
          items:
            type: object
        maliciousOrigins:
          type: array
          description: 恶意包来源记录；漏洞公告通常为空。
          items:
            type: object
        publishedAt:
          type: string
          format: date-time
          nullable: true
        modifiedAt:
          type: string
          format: date-time
          nullable: true
        withdrawnAt:
          type: string
          format: date-time
          nullable: true
          description: 公告撤回时间；未撤回时为 null。

    SecurityAdvisory:
      allOf:
        - $ref: "#/components/schemas/SecurityAdvisoryBase"
        - type: object
          properties:
            packageImpacts:
              type: array
              description: 受影响包、范围和修复版本。
              items:
                $ref: "#/components/schemas/PackageImpact"
            cveEnrichments:
              type: array
              description: CVE 扩展数据。
              items:
                $ref: "#/components/schemas/CveEnrichment"

    PackageImpact:
      type: object
      description: 一条公告对某个包的影响范围。
      properties:
        ecosystem:
          type: string
          enum: [npm, pypi, go, crates-io]
        packageName:
          type: string
          description: 上游公告中的包名。
        packageKey:
          type: string
          description: VibeGuard 内部规范化后的包键。
        purl:
          type: string
          nullable: true
          description: Package URL，存在时可用于跨工具关联。
        affectedVersions:
          type: array
          description: 上游显式列出的受影响版本。
          items:
            type: string
        ranges:
          type: array
          description: OSV ranges 原始结构，包含 introduced/fixed/last_affected 等事件。
          items:
            type: object
        fixedVersions:
          type: array
          description: 已知修复版本。
          items:
            type: string

    CveDetail:
      type: object
      description: 单个 CVE 的富集信息和关联公告。
      properties:
        cveId:
          type: string
          example: CVE-2026-25639
        enrichment:
          oneOf:
            - $ref: "#/components/schemas/CveEnrichment"
            - type: "null"
        advisories:
          type: array
          items:
            $ref: "#/components/schemas/SecurityAdvisory"

    CveEnrichment:
      type: object
      description: 来自 NVD、FIRST EPSS、CISA KEV 的 CVE 扩展字段。
      properties:
        cveId: { type: string }
        title: { type: string, nullable: true, description: NVD 或来源标题。 }
        description: { type: string, nullable: true, description: CVE 描述。 }
        cvssMetrics:
          {
            type: array,
            description: CVSS 指标原始结构。,
            items: { type: object },
          }
        bestCvssScore:
          { type: string, nullable: true, description: 选出的最高 CVSS 分数。 }
        bestCvssSeverity:
          { type: string, nullable: true, description: 最高 CVSS 严重度。 }
        cweIds:
          { type: array, description: CWE 分类。, items: { type: string } }
        epss: { type: string, nullable: true, description: FIRST EPSS 分数。 }
        epssPercentile:
          { type: string, nullable: true, description: FIRST EPSS percentile。 }
        epssScoreDate: { type: string, format: date-time, nullable: true }
        epssModelVersion: { type: string, nullable: true }
        kevListed: { type: boolean, description: 是否列入 CISA KEV。 }
        kevDateAdded: { type: string, format: date-time, nullable: true }
        kevDueDate: { type: string, format: date-time, nullable: true }
        kevKnownRansomwareCampaignUse: { type: string, nullable: true }
        kevRequiredAction: { type: string, nullable: true }
        kevVendorProject: { type: string, nullable: true }
        kevProduct: { type: string, nullable: true }
        kevNotes: { type: string, nullable: true }
        nvdPublishedAt: { type: string, format: date-time, nullable: true }
        nvdModifiedAt: { type: string, format: date-time, nullable: true }

    SecuritySyncStatusResponse:
      type: object
      description: 安全数据源同步状态，用于判断本地镜像是否新鲜。
      properties:
        meta:
          type: object
          properties:
            sourceCount:
              type: integer
              description: 同步状态记录数量。
            staleAfterMs:
              type: integer
              description: 判定 stale 的毫秒阈值。
        items:
          type: array
          items:
            type: object
            properties:
              source:
                {
                  type: string,
                  description: 数据源，例如 osv、nvd、epss、kev。,
                }
              scope: { type: string, description: 同步范围。 }
              status: { type: string, enum: [idle, running, success, failed] }
              lastProcessedModifiedAt:
                { type: string, format: date-time, nullable: true }
              cursorJson: { type: object, description: 增量同步游标。 }
              lastStartedAt: { type: string, format: date-time, nullable: true }
              lastSuccessAt: { type: string, format: date-time, nullable: true }
              lastError: { type: string, nullable: true }
              recordsSeen: { type: integer }
              recordsImported: { type: integer }
              recordsFailed: { type: integer }
              updatedAt: { type: string, format: date-time }
              stale: { type: boolean, description: 该来源是否超过新鲜度阈值。 }
