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截图, 依赖图分析, 分布式系统, 响应大小分析, 大规模测量, 安全测量, 容器安全, 容器生态, 日志审计, 爬虫框架, 版权保护, 网络安全, 请求拦截, 隐私保护, 风险优先级排序