CristhianKapelinski/DITector
GitHub: CristhianKapelinski/DITector
一个用于大规模采集 Docker Hub 镜像元数据、构建依赖关系图并生成安全扫描优先级的自动化流水线框架。
Stars: 1 | Forks: 0
# DITector — 大规模 Docker Hub 安全研究流水线
## 索引
1. [背景与动机](#1-contexto-e-motivação)
2. [流水线架构](#2-arquitetura-da-pipeline)
3. [科学方法论(Dr. Docker 论文)](#3-metodologia-científica)
4. [本分支的修改内容](#4-o-que-este-fork-modifica)
5. [前置条件与配置](#5-pré-requisitos-e-configuração)
6. [config.yaml 配置](#6-configuração-do-configyaml)
7. [阶段 I — 爬取(发现)](#7-estágio-i--crawling-descoberta)
8. [阶段 II — 构建(IDEA 图)](#8-estágio-ii--build-grafo-idea)
9. [阶段 III — 排序(优先级)](#9-estágio-iii--rank-priorização)
10. [OpenVAS 集成](#10-integração-com-openvas)
11. [流水线自动化](#11-automação-da-pipeline)
12. [监控](#12-monitoramento)
13. [命令参考](#13-referência-de-comandos)
14. [设计决策与权衡](#14-decisões-de-design-e-trade-offs)
## 1. 背景与动机
本项目实现了 Docker 镜像的收集与排序,用于使用 **OpenVAS** 进行动态安全扫描。目标是智能选择约 100,000 个 Docker Hub 容器镜像——而非随机选择——优先选择以下镜像:
- **高 Pull Count**(广泛使用,对用户有直接影响)
- **高依赖权重**(基础镜像,其漏洞会传播到子镜像)
- **网络暴露**(通过 `EXPOSE` 配置了网络服务的容器,是 OpenVAS 扫描的候选对象)
科学基础是论文 **"Dr. Docker: A Large-Scale Security Measurement of Docker Image Ecosystem"**(WWW '25,Shi 等,上海交通大学),该论文提出了 **DITector** 框架,用于大规模测量 Docker 镜像生态系统的安全性。
## 2. 流水线架构
```
┌──────────────────────────────────────────────────────────────────────────┐
│ DITector Research Pipeline │
└──────────────────────────────────────────────────────────────────────────┘
NÓ 1 / NÓ 2 (Crawlers) NÓ 1 (Bancos de Dados)
┌──────────────────┐ ┌──────────────────────┐
│ Docker Hub │ │ MongoDB │
│ V2 API │────────────────▶│ (repositories_data) │
│ /v2/search/ │ │ namespace, name, │
│ Stage I: CRAWL │ │ pull_count │
│ (DFS + Workers) │ └──────────┬───────────┘
└──────────────────┘ │
│
NÓ 3 (Builder) │
┌──────────────────┐ ┌──────────▼───────────┐
│ Docker Hub │ │ Stage II │
│ Tag+Image API │────────────────▶│ BUILD │
│ (JWT authn, │ │ Claim atômico + │
│ HubClient, │ │ HubClient + cache │
│ cache MongoDB) │ │ + Neo4j IDEA │
└──────────────────┘ └──────────┬───────────┘
│
┌──────────▼───────────┐
│ Neo4j │
│ (Layer IDEA graph) │
│ IS_BASE_OF edges │
│ ./neo4j_data/ │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Stage III │
│ RANK │
│ Dependency Weight │
│ + Pull Count sort │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ final_prioritized_ │
│ dataset.json │
│ (JSONL, one record │
│ per image) │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ OpenVAS Scanning │
│ (seu script externo)│
└──────────────────────┘
```
## 3. 科学方法论
Dr. Docker 论文(WWW '25)定义如下:
### 3.1 数据收集
Docker Hub 提供两种类型的仓库:
- **官方镜像**:通过公共索引文件列出(`docker-library/official-images`)
- **社区镜像**:通过 API `GET /v2/search/repositories/?query=` 访问
API 接受 2-255 个字符的查询,每个查询最多返回 **10,000** 个结果。为了覆盖 12M+ 仓库,论文实现了一个 **DFS 关键词生成器**:
```
Se count(keyword) >= 10.000 → aprofundar: enfileirar keyword+"a", keyword+"b", ..., keyword+"z", keyword+"0", ..., keyword+"9", keyword+"-", keyword+"_"
Se count(keyword) < 10.000 → scrape: pegar todas as páginas disponíveis
```
### 3.2 IDEA 图构建(Image DEpendency grAph)
该图通过 **Layer 节点** 建模镜像之间的继承关系。每个节点从依赖链的角度表示一个层。
**节点 ID 计算:**
对于 **content layer**(具有 `digest`):
```
dig_i = SHA256(layer_i.digest)
Layer_i.id = SHA256(Layer_{i-1}.id + dig_i)
```
对于 **config layer**(只有 Dockerfile 指令,没有文件内容):
```
dig_i = SHA256(layer_i.instruction)
Layer_i.id = SHA256(Layer_{i-1}.id + dig_i)
```
**bottom layer**(i=0)的节点 ID 使用 `preID = ""` 计算:
```
Layer_0.id = SHA256("" + SHA256(layer_0.digest_or_instruction))
```
**为什么这个方案有效:** 如果两个镜像共享相同的前 N 个层且顺序相同,它们将共享相同的 `Layer_N.id`。这允许通过图识别上游/下游,而无需比较所有层。
**图中的关系:**
- `(Layer)-[:IS_BASE_OF]->(Layer)` — 层之间的继承关系
- `(Layer)-[:IS_SAME_AS]-(RawLayer)` — 层位置到物理内容的关联
### 3.3 关键镜像识别
论文定义了两组高影响力镜像:
| 类型 | 标准 | 论文中的数量 |
|------|----------|--------------|
| **High-Pull-Count** | Pull count ≥ 1,000,000,最近的 top 3 标签 | 20,673 个镜像 |
| **High-Dependency-Weight** | 依赖权重 ≥ 10(≥10 个镜像直接依赖) | 25,924 个镜像 |
**依赖权重(出度):** 继承自该镜像的子镜像数量。
**被依赖权重(入度):** 该镜像依赖的镜像数量。
### 3.4 论文发现
- **93.7%** 的分析镜像包含已知漏洞
- **4,437** 个镜像存在secret泄露(私钥、API 令牌、URI)
- **50** 个镜像存在配置错误(MongoDB、Redis、Elasticsearch、CouchDB)
- **24** 个恶意镜像(加密矿工:XMR、PKT、CRP)
- **334** 个下游镜像受恶意镜像影响(通过供应链传播)
## 4. 本分支的修改内容
原始上游(`NSSL-SJTU/DITector`)声明了 `crawl` 子命令但未实现(对应的 `cobra.Command` 缺少 `Run` 字段)。阶段 II 和阶段 III 可以正常工作。本分支实现了完整的阶段 I,并对阶段 II 进行了大规模并行运行的重构。
### 4.1 新增 `crawler/` 包
**文件:** `crawler/crawler.go`
实现了论文中描述的分布式爬虫。原始上游在 `cmd/cmd.go` 中声明了 `crawl` 子命令但缺少 `Run` 字段——该命令是一个注册的无实现占位符。本分支实现了阶段 I 的完整逻辑。
**任务队列架构:**
- `ParallelCrawler` 维护 N 个 worker,从 MongoDB 集合 `crawler_keywords` 中消费任务
- 每个任务是一个带有 `status` 字段的 DFS 前缀:`pending` → `processing` → `done`
- `getNextTask()` 使用原子 `FindOneAndUpdate`,确保两个 worker(包括在不同节点上)永远不会同时处理相同的前缀
- `ensureQueueInitialized()` 仅在集合为空时将字母表 `[a-z0-9-_]` 作为 `pending` 种子;在重启时,将 `processing` 任务恢复为 `pending`(崩溃后的自愈)
- `processTask()`:收集该前缀的所有页面(最多 100 页 × 100 个结果),然后如果 `count >= 10,000` 或 `len(prefix) == 1`(停用词规避),则插入 38 个子任务为 `pending`
- 内存去重通过 `seenRepos sync.Map` 实现(O(1));`PreloadExistingRepos()` 在启动时通过将所有仓库名称加载到 RAM 来预热缓存
**反检测策略 — `fetchPage`:**
Docker Hub 使用 WAF/Cloudflare 进行行为检测。响应采用多层伪装:
| 层 | 机制 | 实现 |
|--------|-----------|---------------|
| TLS 指纹 | 强制 HTTP/1.1(无 HTTP/2),TLS 1.2+ | `tls.Config{MinVersion: tls.VersionTLS12}` + 无 HTTP/2 的传输层 |
| 浏览器 Headers | Chrome 121 完整 Headers 集 | `setBrowserHeaders()`:UA、Accept、Referer、Sec-Fetch-*、Connection |
| 账户身份 | 每个 JWT 账户有固定且唯一的 UA | `acc.UserAgent` 在启动时通过 7 个 UA 的轮询分配 |
| 页面间抖动 | 页面间随机 400–900 ms | 每次请求 `rand.Intn(500) + 400` ms |
| 任务间抖动 | 每个任务后随机 0–1000 ms | worker 循环中 `rand.Intn(1000)` ms |
| Keep-Alive / body draining | 关闭前完全读取 body | `io.ReadAll(resp.Body)` — 将 socket 返回 TCP 池 |
**HTTP 错误处理 — 无递归重试:**
| 代码 | 解释 | 操作 | 任务目标 |
|--------|---------------|------|-------------------|
| 401 | JWT 过期 | `ClearToken(token)` + `GetNextClient()` | 重新入队为 `pending` |
| 403 | Bot 分数高 / IP 被捕获 | sleep 15 min + `GetNextClient()` | 重新入队为 `pending` |
| 429 | IP/账户速率限制 | sleep 15 s + `GetNextClient()` | 重新入队为 `pending` |
| 其他 | 临时错误 | 返回 `nil` | 重新入队为 `pending` |
任务永远不会被丢弃:在任何失败情况下,`processTask` 在返回前调用 `updateTaskStatus(prefix, "pending")`。在任何可用的 worker 的下一次迭代中,它将被恢复。
**文件:** `crawler/auth_proxy.go`
`IdentityManager` 集中管理身份验证、User-Agent 和 HTTP 客户端:
- 从 `accounts.json` 加载 Docker Hub 账户`[{username, password}]`)
- 在加载时为每个账户分配独特的 User-Agent — 在 `globalUAPool`(7 个字符串,代表 Chrome 121、Edge、Firefox 122、Safari 17 在 Windows、Mac 和 Linux 上)上进行轮询
- `GetNextClient()` 返回 `(*http.Client, token, ua)`:UA 与 token 一起返回,以便在该身份的所有请求中一致传播
- 通过 `POST /v2/users/login/` 获取 JWT,受 `loginMu sync.Mutex` 保护 — 防止两个 worker 同时登录同一账户
- `ClearToken(token string)` 遍历账户并将相应账户的 `Token` 字段置零;下次调用 `GetNextClient` 时,将自动调用 `LoginDockerHub`
- 每个客户端的 `http.Transport` 配置 `MaxIdleConns=100`、`IdleConnTimeout=90s` 和 `TLSHandshakeTimeout=10s`,保持稳定的 TCP 池,避免大量打开 socket(机器人特征)
### 4.2 新增文件 `buildgraph/from_mongo.go`
对 `build` 阶段进行了完全重构,支持使用 MongoDB 原子 claim 的分布式运行:
```
ClaimNextBuildRepo (por goroutine — FindOneAndUpdate atômico)
│
▼ repoWorker × max(NumCPU*8, 32) ← I/O bound: espera de rede
│ (HubClient autenticado por goroutine)
│ (cache MongoDB → fallback API para tags e imagens)
│ (defer MarkRepoGraphBuilt — sempre executado)
▼
jobChan
│
▼ graphWorker × max(NumCPU*2, 8) ← DB bound: escrita Neo4j
│
▼
Neo4j (grafo IDEA) + MongoDB (graph_built_at)
checkpointWriter (goroutine single-writer)
▼
dataDir/build_checkpoint.jsonl
```
**原子 claim:** 每个 `repoWorker` 使用 `ClaimNextBuildRepo` 而不是共享游标,支持在多台机器上分布式运行。`ResetStaleBuildClaims` 在启动时释放之前运行中遗留的 claim。
**检查点:** `processRepo` 中的 `defer MarkRepoGraphBuilt` 确保为所有已处理的仓库写入 `graph_built_at`,包括没有可用标签的仓库——消除空仓库的无限重处理。
### 4.3 修改 `myutils/urls.go`
V2 Search API 的模板和函数:
```
V2SearchURLTemplate = `https://hub.docker.com/v2/search/repositories/?query=%s&page=%d&page_size=%d`
func GetV2SearchURL(query string, page, size int) string
```
`ordering=-pull_count` 参数已被移除。Docker Hub 使用 `best_match` 作为默认排序模式,该模式优先考虑精确前缀匹配,然后才是按流行度排序的结果。对于 DFS 前缀遍历,`best_match` 在语义上更优越:`query="ngin"` 会先返回 `nginx`,然后才是仅在描述中提到 "nginx" 的仓库,最大化了在每个 DFS 树节点收集结果的相关性。
页面间的一致性由 MongoDB 的 `{namespace, name}` 唯一索引保证,而不是按到达顺序。
上游将 `crawl` 子命令声明为无实现的占位符,未使用任何搜索 API。
### 4.4 新增文件 `myutils/hubclient.go`
`HubClient` 是阶段 I 和阶段 II 共享的认证 HTTP 客户端,消除了代码重复:
- **`IdentityProvider` 接口** — 抽象 `IdentityManager`;允许 `myutils` 不依赖 `crawler`
- **`NewHubClient(ip IdentityProvider) *HubClient`** — 每个 goroutine 一个实例
- **`Get(url)`** — 3 次尝试,401/429/403 时轮换;自动注入 Chrome 145 headers
- **`GetInto(url, dest)`** — `Get` + JSON 反序列化
- **`GetTags(ns, name, pageNum, size)`** — 认证的分页标签查询
- **`GetImages(ns, name, tag)`** — 认证的镜像 manifest 查询
- **`setHeaders(req)`** — 注入 `Accept-Language: pt-BR,pt;q=0.9,...`、`Referer: https://hub.docker.com/`、`Sec-Fetch-*`
- **`rotate()`** — 通过 `IdentityProvider` 内部切换身份
### 4.5 新增文件 `buildgraph/metrics.go`
`BuildMetrics` 为阶段 II 提供实时进度跟踪:
- 原子计数器:已处理、标签缓存命中/未命中、镜像缓存命中/未命中、Neo4j 插入、错误
- `newBuildMetrics(threshold)` 在启动时捕获待处理仓库总数
- `startReporter(dataDir, done)` 每 60 秒记录并持久化到 `build_metrics.log`
- 30 秒后计算 ETA:`rate = processed/elapsed_min`,`ETA = (total-processed)/rate`
### 4.6 修改 `myutils/mongo.go`
为支持高吞吐量爬虫和分布式阶段 II,为 MongoDB 客户端添加了:
- **`BulkUpsertRepositories(repos []*Repository)`** — 原子无序批量写入;比循环中的单独 upsert 快约 10-50 倍,可一次性处理整页结果
- **`KeywordsColl`** — 新集合 `crawler_keywords` 用于阶段 I 检查点:重启时,已完全爬取的关键词在 O(1) 时间内被忽略
- **`IsKeywordCrawled(keyword)` / `MarkKeywordCrawled(keyword)`** — 阶段 I 检查点的读写接口
- **`MarkRepoGraphBuilt(namespace, name)`** — 写入 `graph_built_at` 并移除 `build_claimed`/`build_started_at`(阶段 II 检查点)
- **`ClaimNextBuildRepo(threshold)`** — 阶段 II 仓库 claim 的原子 `FindOneAndUpdate`
- **`ResetStaleBuildClaims()`** — 阶段 II 启动时释放遗留 claim
- **`CountPendingBuildRepos(threshold)`** — 检查队列是否为空,用于 immortal worker 模式
- **`FindImagesByDigests(digests)`** — 使用 `$in` 的批量查询;替换 N 个单独查询
- **连接池**:`SetMaxPoolSize(100)`、`SetMinPoolSize(5)`、`SetMaxConnIdleTime(5m)` — 高并行负载下的稳定性
- **初始 ping 超时**:从 `1s` 增加到 `30s` — 避免慢连接下的误报
### 4.7 修改 `myutils/neo4j.go`
`InsertImageToNeo4j` 已重写为**每个镜像单个事务**(之前:每个层一个事务):
1. 所有层 ID 通过 SHA256 本地计算(纯 CPU,零网络 I/O)
2. 整个层链 + 镜像标签插入到**单个 `ExecuteWrite`** 中 — 每个镜像独立于层数量的 O(1) 轮次
结果:插入延迟从 O(N layers × RTT) 降至 O(1 × RTT)。
**修复 `findLayerNodesByRawLayerDigestFunc`:原始查询使用 `{id: $digest}` 匹配 `RawLayer` 节点,但存储的属性是 `digest`。已修复为 `{digest: $digest}`。该 bug 导致上游镜像跟踪静默失败。
### 4.8 修改 `myutils/docker_hub_api_requests.go`
全局 HTTP 客户端已重构:
- **移除 `DisableKeepAlives: true`** — 启用 keep-alive;TCP 连接在请求间复用(每个请求节省约 100-300ms 的握手+TLS 时间)
- **连接池**:`MaxIdleConns: 300`、`MaxIdleConnsPerHost: 50`、`IdleConnTimeout: 90s`
- **全局客户端添加 `Timeout: 30s`**
### 4.9 修改 `myutils/config.go`
- **环境变量覆盖**:`MONGO_URI` 和 `NEO4J_URI` 覆盖 `config.yaml` 中的值 — 允许运行 Node 2 指向 Node 1 的 MongoDB 而无需修改配置文件
- **配置位置**:`filepath.Dir(os.Args[0])` → `os.Getwd()` — 配置相对于工作目录查找,而不是相对于二进制文件(与 `go run` 兼容)
- **Neo4j 可选**:如果 Neo4j 连接在初始化时失败,系统不会中止 — 适用于仅运行阶段 I 而不启动 Neo4j
### 4.10 Docker Compose 基础设施
运行流水线的完整基础设施:
| 服务 | 镜像 | 端口 | 用途 |
|---------|--------|-------|-----------|
| `ditector_mongo` | `mongo:latest` | 27017 | 仓库、标签、镜像的持久化 |
| `ditector_neo4j` | `neo4j:latest` | 7474/7687 | IDEA 依赖图 |
| `ditector_crawler` | `golang:1.22` | — | 使用可配置种子执行爬取 |
环境变量 `SEED` 允许运行多个具有不同种子的爬虫实例(中间相遇策略):
```
SEED=a docker compose up -d crawler # Máquina 1: a-m
SEED=n docker compose up -d crawler # Máquina 2: n-z
```
`docker-compose.node3.yml` 为节点 3(阶段 II)定义 `builder` 服务:
```
DB_HOST= NEO4J_URI=neo4j://:7687 make start-build
```
Neo4j 卷已从命名 Docker 卷迁移到主机路径 `./neo4j_data:/data`,保护数据免受 `docker system prune -a --volumes` 影响。
### 411 修改 `scripts/calculate_node_dependent_weights.go`
`if repoDoc.Namespace == "library"` 分支的第一个指令是 `continue`,导致下面所有代码无法到达。官方 Docker 镜像(`library/`)在依赖权重计算中被静默忽略。`continue` 已被移除。
### 4.12 新增自动化脚本(`automation/`)
- `pipeline_autopilot.sh` — 使用参数化配置顺序执行 3 个阶段
- `test_e2e.sh` — 端到端集成测试:使用种子 `nginx` 爬取、构建、排序,验证输出
## 5. 前置条件与配置
### 所需软件
```
# Go 1.21+
go version
# Docker 和 Docker Compose
docker --version
docker compose version
```
### 基础设施
在任何命令之前启动 MongoDB 和 Neo4j:
```
docker compose up -d mongodb neo4j
```
等待约 10 秒让服务启动。验证:
```
# MongoDB
mongosh localhost:27017 --eval "db.runCommand({ping: 1})"
# Neo4j
curl -s http://localhost:7474 | head -5
```
### Docker Hub 账户(爬取所需)
在项目根目录创建 `accounts.json`(**不要提交**):
```
[
{"username": "usuario1", "password": "senha1"},
{"username": "usuario2", "password": "senha2"}
]
```
### 代理(可选)
在根目录创建 `proxies.txt`(每行一个 URL):
```
http://user:pass@proxy1.example.com:8080
http://user:pass@proxy2.example.com:8080
socks5://proxy3.example.com:1080
```
## 6. config.yaml 配置
复制模板并调整:
```
cp config_template.yaml config.yaml
```
主要字段:
```
max_thread: 0 # 0 = usa todos os CPUs disponíveis
log_file: "ditector.log" # caminho relativo à raiz do projeto
mongo_config:
uri: "mongodb://localhost:27017"
database: "dockerhub_data"
collections:
repositories: "repositories_data"
tags: "tags_data"
images: "images_data"
image_results: "image_results"
layer_results: "layer_results"
user: "user_data"
neo4j_config:
neo4j_uri: "neo4j://localhost:7687"
neo4j_username: "neo4j"
neo4j_password: "" # vazio se NEO4J_AUTH=none (docker-compose default)
proxy:
http_proxy: "" # deixe vazio se não usar proxy global
https_proxy: ""
```
## 7. 阶段 I — 爬取(发现)
### Docker Hub 仓库名称表示
Docker Hub 将镜像组织为两个层级:`namespace/name`。没有嵌套命名空间(与 GitHub 不同)。V2 API 返回的 `repo_name` 字段有两种可能格式:
| 类型 | API 中的 `repo_name` | 实际命名空间 | 实际名称 |
|------|--------------------|----------------|-----------|
| 官方镜像(`library`) | `"nginx"` | `library` | `nginx` |
| 官方镜像(`library`) | `"postgres"` | `library` | `postgres` |
| 社区镜像 | `"cimg/postgres"` | `cimg` | `postgres` |
| 社区镜像 | `"redis/redis-stack"` | `redis` | `redis-stack` |
API 响应中存在的 `repo_name` 字段**始终为空**(`""`)对于所有仓库类型——不应使用。正确的 `namespace` 仅通过 `crawler/crawler.go` 中的 `parseRepoName()` 从 `repo_name` 提取:
```
func parseRepoName(repoName string) (namespace, name string) {
parts := strings.SplitN(repoName, "/", 2)
if len(parts) == 2 {
return parts[0], parts[1] // community: "nginx/nginx-ingress" → ("nginx", "nginx-ingress")
}
return "library", repoName // oficial: "nginx" → ("library", "nginx")
}
```
**为什么这对于 `docker pull` 和 OpenVAS 至关重要:**
- `library/` 镜像:命名空间可以省略。`docker pull nginx` 等同于 `docker pull library/nginx`。
- 社区镜像:命名空间**是必需的**。`docker pull cimg/postgres` 没有前缀 `cimg/` 无法工作。没有它,Docker 会解释为 `library/postgres`——不同的镜像,扫描结果无效。
从导出的数据集生成 pull 名称的正确格式:
```
ns = record["repository_namespace"]
img = record["repository_name"]
tag = record["tag_name"]
# 对于 library 镜像,pull 时省略 namespace(Docker 约定)
image_ref = f"{img}:{tag}" if ns == "library" else f"{ns}/{img}:{tag}"
# docker pull nginx:latest ← library
# docker pull cimg/postgres:15 ← community
```
**经验验证:** 在对 V2 API 1000 个结果(涵盖 10 个不同查询:`nginx`、`redis`、`postgres`、`mysql`、`debian`、`ubuntu`、`python`、`node`、`go`、`java`)的抽样中,没有任何 `repo_name` 包含多个斜杠。`namespace/name` 格式是 Docker Hub 的结构上限。
### 功能
爬虫使用 **DFS(深度优先搜索)** 策略遍历关键词空间,发现仓库并将 `namespace`、`name` 和 `pull_count` 持久化到 MongoDB。
**内部流程:**
```
seed keyword
│
▼
GET /v2/search/repositories/?query=&page=1&page_size=100
│
├─ count >= 10.000? → enfileirar keyword+[a-z0-9-_] (aprofundar DFS)
├─ count > 0? → scrapeAllPages: coletar todas as páginas
└─ count == 0? → keyword sem resultados, avançar
```
### 运行方式
**简单模式(单台机器):**
```
go run main.go crawl \
--workers 20 \
--accounts accounts.json \
--config config.yaml
```
**加速模式(多台机器 / 中间相遇):**
```
# 机器 1:种子 a-m
go run main.go crawl --workers 30 --seed 'a' --accounts accounts.json --config config.yaml
# 机器 2:种子 n-z
go run main.go crawl --workers 30 --seed 'n' --accounts accounts.json --config config.yaml
```
**使用代理:**
```
go run main.go crawl --workers 20 --proxies proxies.txt --accounts accounts.json --config config.yaml
```
### 参数
| 标志 | 默认值 | 描述 |
|------|--------|-----------|
| `--workers` / `-w` | 10 | 并行工作 goroutine 数量 |
| `--seed` | — | DFS 初始关键词,逗号分隔(无种子则从整个字母表开始) |
| `--shard` | -1 | 分片索引(从 0 开始)用于分布式爬取;需要 `--shards` |
| `--shards` | 1 | 用于中间相遇分布的分片总数(例如:2 表示在 2 台机器间分割字母表) |
| `--accounts` | — | `accounts.json` 路径 |
| `--proxies` | — | 代理文件路径(每行一个 URL) |
| `--config` / `-c` | `config.yaml` | 配置文件路径 |
### 检查进度
```
# 发现仓库计数
mongosh localhost:27017/dockerhub_data --eval 'db.repositories_data.countDocuments()'
# 实时跟踪发现
tail -f *.log | grep "Discovered repository"
# 按 pull_count 排名前 10
mongosh localhost:27017/dockerhub_data --eval '
db.repositories_data.find({}, {name:1, pull_count:1, _id:0})
.sort({pull_count: -1}).limit(10).pretty()
'
```
### 预期量
1 台机器 20 个 worker 运行 24 小时,预计发现 500,000 到 2,000,000 个仓库,具体取决于连接速度和速率限制。Docker Hub 共有 12M+ 仓库。
## 8. 阶段 II — 构建(IDEA 图)
### 功能
对于 MongoDB 中 `pull_count >= threshold` 的每个仓库,阶段 II:
1. 通过 `ClaimNextBuildRepo`(MongoDB `FindOneAndUpdate`)原子地 claim 仓库,确保没有其他 worker 同时处理它
2. 查询 MongoDB 标签缓存;仅当缓存不包含数据时才通过 JWT 认证(HubClient)调用 Docker Hub API
3. 对于每个标签,查询 MongoDB 镜像缓存;必要时访问 API 获取层(digest、instruction、size)
4. 过滤 Windows 镜像
5. 使用层 ID 哈希算法将 IDEA 图插入 Neo4j(论文第 3.2 节)
6. 通过 `MarkRepoGraphBuilt`(`graph_built_at` 字段)将仓库标记为完成——通过 `defer` 执行,因此即使对于 0 标签的仓库也能保证
阶段 II 可以在多台机器上同时运行。原子 claim 消除了节点间无需额外协调的重复处理。
### 运行方式
**通过 Makefile(节点 3 — 推荐):**
```
# 配置变量并启动 builder 容器
DB_HOST= NEO4J_URI=neo4j://:7687 make start-build
# 跟踪日志
make logs-build
```
**通过命令行(开发 / 本地测试):**
```
go run main.go build \
--format mongo \
--threshold 1000 \
--tags 3 \
--accounts accounts.json \
--data_dir /tmp/ditector_build \
--config config.yaml
```
### 参数
| 标志 | 默认值 | 描述 |
|------|--------|-----------|
| `--format` | `mongo` | 数据源(仅支持 `mongo`) |
| `--threshold` | 1,000,000 | 处理仓库的最小 pull count |
| `--tags` | 10 | 每个仓库处理的最多最近标签数 |
| `--accounts` | — | `accounts.json` 路径(JWT 认证 — 与阶段 I 相同的文件) |
| `--proxies` | — | 代理文件路径(可选) |
| `--data_dir` | `.` | `build_checkpoint.jsonl` 和 `build_metrics.log` 的目录 |
`--page` 和 `--page_size` 参数已被移除:进度控制通过 MongoDB 中的 `graph_built_at` 字段(通过原子 claim)管理,而不是手动分页。
**研究建议:**
- `--threshold 1000` — 覆盖大多数有实际活动的仓库
- `--tags 3` — 与 Dr. Docker 论文一致;最近 3 个标签足以进行继承分析
### 进度监控
```
# 实时 ETA 指标
tail -f build_metrics.log
# 指标行示例:
# [METRICS 02:15:00] progresso=1234/48000 (2.6%) | taxa=45.2 repos/min | ETA=17h22m | cache tags=82% imgs=71% | neo4j=12340 | erros=3 | uptime=27m18s
# 已完成仓库(checkpoint 行)
wc -l build_checkpoint.jsonl
# MongoDB 直接计数
mongosh /dockerhub_data --eval \
'db.repositories_data.countDocuments({graph_built_at: {$exists: true}})'
# Neo4j 节点
cypher-shell -u neo4j -p "" "MATCH (l:Layer) RETURN count(l) AS total_layers"
# Neo4j 边
cypher-shell -u neo4j -p "" "MATCH ()-[r:IS_BASE_OF]->() RETURN count(r) AS total_edges"
```
### Neo4j 数据持久化
Neo4j 持久化到 `./neo4j_data/`(显式主机路径)。该文件夹在首次启动时由 Docker Compose 自动创建。与命名 Docker 卷不同,它不受 `docker system prune -a --volumes` 影响。请将 `neo4j_data/` 与 `mongo_data_secure/` 一起纳入常规备份。
## 9. 阶段 III — 排序(优先级)
### 功能
对于 Neo4j 图中处理的每个镜像,计算**依赖权重**(IDEA 中的出度)——继承自该镜像的下游镜像数量——并导出 JSONL 结果文件。
**输出模式(每行一个 JSON):**
```
{
"repository_namespace": "library",
"repository_name": "nginx",
"tag_name": "latest",
"image_digest": "sha256:abc123...",
"weights": 1847,
"downstream_images": ["user1/app:latest", "user2/service:v2", ...]
}
```
### 运行方式
```
go run main.go execute \
--script calculate-node-weights \
--threshold 1000 \
--file final_prioritized_dataset.json \
--config config.yaml
```
### OpenVAS 后处理
按依赖权重(降序)和 pull count 排序以确定优先级:
```
# 按依赖权重排名前 100
jq -s 'sort_by(-.weights) | .[0:100]' final_prioritized_dataset.json
# 提取镜像名称用于扫描
jq -r '"\(.repository_namespace)/\(.repository_name):\(.tag_name)"' final_prioritized_dataset.json \
| sort -u \
> images_for_openvas.txt
```
## 10. OpenVAS 集成
流水线的最终目标是为 OpenVAS 扫描器提供网络容器。流程是:
```
images_for_openvas.txt
│
▼
[seu script de scanning]
1. docker pull
2. docker run -d --name scan_target
3. docker inspect scan_target → pegar IP do container
4. openvas-cli --target --scan-config "Full and Fast"
5. coletar relatório
6. docker rm -f scan_target
7. próxima imagem
```
**没有网络服务的容器:** 如果容器不暴露端口或不运行网络守护进程,OpenVAS 不会发现服务。外部扫描脚本应处理此情况,继续处理下一个容器。
## 11. 流水线自动化
### 流水线自动驾驶
顺序执行 3 个阶段:
```
./automation/pipeline_autopilot.sh "a"
```
脚本中的配置:
```
WORKERS=20 # workers de crawl
CRAWL_DURATION="30s" # tempo de crawl (ajuste para pesquisa real: "6h", "24h")
PULL_THRESHOLD=1000 # pull count mínimo
OUTPUT_FILE="final_prioritized_dataset.json"
```
### 端到端集成测试
验证整个流水线使用真实数据(种子 `nginx`)正常工作
```
chmod +x automation/test_e2e.sh
./automation/test_e2e.sh
```
测试验证的内容:
1. 使用种子 `nginx` 爬取 20 秒 → 发现 nginx 相关仓库
2. 使用 threshold=0 构建 → 处理所有发现的仓库
3. 排序 → 生成 `test_output.json`
4. 验证 `test_output.json` 存在且大小 > 10 字节
## 12. 监控
### MongoDB
```
# 发现仓库总数
mongosh localhost:27017/dockerhub_data --eval \
'db.repositories_data.countDocuments()'
# pull_count >= 1M 的仓库
mongosh localhost:27017/dockerhub_data --eval \
'db.repositories_data.countDocuments({pull_count: {$gte: 1000000}})'
# 按 pull count 排名前 20 仓库
mongosh localhost:27017/dockerhub_data --eval \
'db.repositories_data.find({},{name:1,namespace:1,pull_count:1,_id:0}).sort({pull_count:-1}).limit(20)'
```
### Neo4j(浏览器访问 http://localhost:7474)
```
// Total de nodes Layer
MATCH (l:Layer) RETURN count(l)
// Total de edges IS_BASE_OF (arestas de dependência)
MATCH ()-[r:IS_BASE_OF]->() RETURN count(r)
// As 10 imagens com mais dependentes
MATCH (l:Layer)-[:IS_BASE_OF*]->(down:Layer)
WHERE size(l.images) > 0
RETURN l.images[0] AS image, count(down) AS downstream
ORDER BY downstream DESC LIMIT 10
// Verificar propagação de ameaças: downstream de nginx:latest
MATCH (src:Layer {id: ''})
MATCH (src)-[:IS_BASE_OF*]->(down:Layer)
WHERE size(down.images) > 0
RETURN down.images
```
### 日志
```
# 实时发现
tail -f *.log | grep "Discovered repository"
# 构建错误
tail -f *.log | grep "ERROR"
# Neo4j 插入速率
tail -f *.log | grep "Inserido no Neo4j" | wc -l
```
## 13. 命令参考
### 可用子命令
```
docker-scan crawl — Fase I: descoberta de repositórios
docker-scan build — Fase II: construção do grafo IDEA
docker-scan analyze — Análise de segurança de uma imagem específica
docker-scan execute — Executa scripts de processamento em lote
docker-scan calculate — Calcula o node ID de uma imagem pelo digest
```
### 全局标志
| 标志 | 默认值 | 描述 |
|------|--------|-----------|
| `--config` / `-c` | `config.yaml` | 配置文件 |
| `--log_level` / `-l` | `debug` | 日志级别:debug、info、warn、error、critical |
### `execute --script`
| 脚本 | 描述 |
|--------|-----------|
| `calculate-node-weights` | 计算每个镜像的依赖权重并导出 JSONL |
| `analyze-threshold` | 分析 pull_count 高于 threshold 的镜像 |
| `analyze-all` | 分析 MongoDB 中的所有镜像 |
| `count-images-with-upstream` | 统计有上游的镜像(入度 > 0) |
| `count-images-with-downstream` | 统计有下游的镜像(出度 > 0) |
| `export-mongo-result-docs` | 将 MongoDB 分析结果导出为 JSON |
| `check-same-node-as-high-dependent-images` | 识别 high-PC 和 high-DW 集合之间的交集 |
## 14. 设计决策与权衡
### 为什么本分支用 Go 实现爬虫?
上游在 `cmd/cmd.go` 中声明了 `crawl` 子命令但缺少 `Run` 字段——已注册但未实现。本分支用 Go 实现阶段 I 是为了保持技术栈一致性,以及 I/O 密集型工作负载的优势:
- **Goroutines**:扩展到数百个 worker,每个约 2KB(vs OS 线程约 1MB)
- **Channels**:阶段间类型安全的通信,无需手动锁
- **单一二进制**:在多台机器上轻松部署,无需外部运行时
### 为什么 Build 调用实时 API 而不是从 MongoDB 读取?
爬虫(阶段 I)仅存储 `namespace`、`name` 和 `pull_count`。标签和层在阶段 II 通过实时 API 获取。刻意权衡:
- **优点**:MongoDB 中的数据量较小;爬虫更快
- **缺点**:构建阶段依赖 API 可用性;爬取和构建之间删除的仓库会产生错误日志
未实现的替代方案:爬虫可以直接存储标签/层,使构建阶段完全离线。
### 已知限制
1. **JWT 过期和重新登录**:收到 HTTP 401 时,`fetchPage` 调用 `ClearToken` 使过期 token 失效,并调用 `GetNextClient` 获取带有自动登录的新身份。如果所有账户同时 token 失效,该页面的重试可能会失败。
2. **构建实时 API**:如果仓库在爬取和构建之间被删除,错误会被记录但不会中断进度。
3. **Neo4j 吞吐量**:每个镜像一个事务(O(1) 轮次)。对于 >1M 镜像的量级,瓶颈会转移到 Neo4j 的堆内存——建议增加 `NEO4J_dbms_memory_heap_max__size`。
*基于论文:Hequan Shi 等,《Dr. Docker: A Large-Scale Security Measurement of Docker Image Ecosystem》,WWW '25.*
标签:Docker Hub, Docker安全, EVTX分析, OpenVAS, PHP, T005, T007, T008, Web截图, 依赖图分析, 分布式系统, 响应大小分析, 大规模测量, 安全测量, 容器安全, 容器生态, 日志审计, 爬虫框架, 版权保护, 网络安全, 请求拦截, 隐私保护, 风险优先级排序