ChimangoScan/DITector
GitHub: ChimangoScan/DITector
一个基于 Docker Hub 大规模测量与依赖图构建的容器安全评估框架,解决镜像选择偏倚与供应链风险优先级问题。
Stars: 1 | Forks: 0
# DITector — Docker 安全流水线大规模部署
## 索引
1. [Contexto e Motivação](#1-contexto-e-motivação)
2. [Arquitetura da Pipeline](#2-arquitetura-da-pipeline)
3. [Metodologia Científica (paper Dr. Docker)](#3-metodologia-científica)
4. [O que este fork modifica](#4-o-que-este-fork-modifica)
5. [Pré-requisitos e Configuração](#5-pré-requisitos-e-configuração)
6. [Configuração do `config.yaml`](#6-configuração-do-configyaml)
7. [Estágio I — Crawling (Descoberta)](#7-estágio-i--crawling-descoberta)
8. [Estágio II — Build (Grafo IDEA)](#8-estágio-ii--build-grafo-idea)
9. [Estágio III — Rank (Priorização)](#9-estágio-iii--rank-priorização)
10. [Integração com OpenVAS](#10-integração-com-openvas)
11. [Automação da Pipeline](#11-automação-da-pipeline)
12. [Monitoramento](#12-monitoramento)
13. [Referência de Comandos](#13-referência-de-comandos)
14. [Decisões de Design e Trade-offs](#14-decisões-de-design-e-trade-offs)
## 1. 上下文与动机
Este projeto implementa a coleta e priorização de imagens Docker para scanning de segurança dinâmico com **OpenVAS**. O objetivo é selecionar ~100.000 containers do Docker Hub de forma inteligente — não aleatoriamente — priorizando imagens com:
- **Alto Pull Count** (amplamente usadas, impacto direto em usuários)
- **Alto Dependency Weight** (imagens base cujas vulnerabilidades se propagam para imagens filhas)
- **Exposição de rede** (containers com serviços de rede configurados via `EXPOSE`, candidatos ao scan OpenVAS)
A base científica é o paper **"Dr. Docker: A Large-Scale Security Measurement of Docker Image Ecosystem"** (WWW '25, Shi et al., Shanghai Jiao Tong University), que propõe o framework **DITector** para medir a segurança do ecossistema Docker em larga escala.
## 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 │
│ │
└──────────────────────┘
```
## 3. 科学方法论
O paper Dr. Docker (WWW '25) define:
### 3.1 数据收集
O Docker Hub fornece dois tipos de repositório:
- **Official images**: listadas via arquivo de índice público (`docker-library/official-images`)
- **Community images**: acessíveis pela API `GET /v2/search/repositories/?query=`
A API aceita queries de 2–255 caracteres e retorna até **10.000 resultados** por query. Para cobrir os 12M+ repositórios, o paper implementa um **DFS keyword generator**:
```
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)
O grafo modela herança entre imagens através de **Layer nodes**. Cada node representa uma camada no ponto de vista da cadeia de dependência.
**Cálculo do node ID:**
Para uma **content layer** (possui `digest`):
```
dig_i = SHA256(layer_i.digest)
Layer_i.id = SHA256(Layer_{i-1}.id + dig_i)
```
Para uma **config layer** (só possui instrução Dockerfile, sem conteúdo de arquivo):
```
dig_i = SHA256(layer_i.instruction)
Layer_i.id = SHA256(Layer_{i-1}.id + dig_i)
```
O node ID do **bottom layer** (i=0) é calculado com `preID = ""`:
```
Layer_0.id = SHA256("" + SHA256(layer_0.digest_or_instruction))
```
**Por que este esquema funciona:** Se duas imagens compartilham as mesmas N primeiras camadas na mesma ordem, elas compartilharão o mesmo `Layer_N.id`. Isso permite identificar upstream/downstream via grafo, sem precisar comparar todos os layers.
**Relações no grafo:**
- `(Layer)-[:IS_BASE_OF]->(Layer)` — relação de herança entre camadas
- `(Layer)-[:IS_SAME_AS]-(RawLayer)` — associação de uma posição de layer ao conteúdo físico
### 3.3 识别关键镜像
O paper define dois conjuntos de imagens de alto impacto:
| Tipo | Critério | Qtd no paper |
|------|----------|--------------|
| **High-Pull-Count** | Pull count ≥ 1.000.000, top 3 tags mais recentes | 20.673 imagens |
| **High-Dependency-Weight** | Dependency weight ≥ 10 (≥10 imagens dependem diretamente) | 25.924 imagens |
**Dependency Weight (Out-Degree):** número de imagens filhas que herdam desta imagem.
**Dependent Weight (In-Degree):** número de imagens das quais esta imagem depende.
### 3.4 论文发现
- **93,7%** das imagens analisadas contêm vulnerabilidades conhecidas
- **4.437** imagens com secret leakage (chaves privadas, tokens de API, URIs)
- **50** imagens com misconfigurações (MongoDB, Redis, Elasticsearch, CouchDB)
- **24** imagens maliciosas (crypto miners: XMR, PKT, CRP)
- **334** imagens downstream afetadas por imagens maliciosas (propagação via supply chain)
## 4. 本分支修改内容
O upstream original (`NSSL-SJTU/DITector`) declarava o subcomando `crawl` mas sem implementação (campo `Run` ausente no `cobra.Command` correspondente). Os estágios II e III estavam funcionais. Este fork implementa o Estágio I completo e reengenharia o Estágio II para operação paralela em larga escala.
### 4.1 新增 `crawler/` 包
**Arquivo:** `crawler/crawler.go`
Implementação do crawler distribuído descrito no paper. O upstream original declarava o subcomando `crawl` em `cmd/cmd.go` mas sem campo `Run` — o comando era um stub registrado sem implementação. Este fork implementa o corpo completo do Estágio I.
**Arquitetura de fila de tarefas:**
- `ParallelCrawler` mantém N workers que consomem tarefas da coleção MongoDB `crawler_keywords`
- Cada tarefa é um prefixo DFS com campo `status`: `pending` → `processing` → `done`
- `getNextTask()` usa `FindOneAndUpdate` atômico, garantindo que dois workers (inclusive em nós distintos) nunca processem o mesmo prefixo simultaneamente
- `ensureQueueInitialized()` semeia o alfabeto `[a-z0-9-_]` como `pending` apenas se a coleção estiver vazia; na reinicialização, reverte tarefas `processing` → `pending` (self-healing após crash)
- `processTask()`: coleta todas as páginas do prefixo (máx. 100 páginas × 100 resultados), depois insere 38 filhos como `pending` se `count >= 10.000` ou `len(prefix) == 1` (stopword workaround)
- Deduplicação em memória via `seenRepos sync.Map` (O(1)); `PreloadExistingRepos()` aquece o cache no boot carregando todos os nomes do banco para RAM
**Estratégia anti-detecção — `fetchPage`:**
O Docker Hub aplica WAF/Cloudflare com detecção comportamental. A resposta é uma pilha de camuflagem em múltiplas camadas:
| Camada | Mecanismo | Implementação |
|--------|-----------|---------------|
| Fingerprint TLS | HTTP/1.1 forçado (sem HTTP/2), 1.2+ | `tls.Config{MinVersion: tls.VersionTLS12}` + transporte sem HTTP/2 |
| Headers de navegador | Conjunto completo de headers Chrome 121 | `setBrowserHeaders()`: UA, Accept, Referer, Sec-Fetch-*, Connection |
| Identidade por conta | Cada conta JWT tem UA fixo e exclusivo | `acc.UserAgent` atribuído no boot via round-robin sobre pool de 7 UAs |
| Jitter entre páginas | 400–900 ms aleatório entre páginas | `rand.Intn(500) + 400` ms por requisição |
| Jitter entre tarefas | 0–1000 ms aleatório após cada tarefa | `rand.Intn(1000)` ms no loop do worker |
| Keep-Alive / body draining | Corpo lido completamente antes de fechar | `io.ReadAll(resp.Body)` — devolve socket ao pool TCP |
**Tratamento de erros HTTP — sem retry recursivo:**
| Código | Interpretação | Ação | Destino da tarefa |
|--------|---------------|------|-------------------|
| 401 | JWT expirado | `ClearToken(token)` + `GetNextClient()` | re-enfileirada como `pending` |
| 403 | Bot score alto / IP flagrado | sleep 15 min + `GetNextClient()` | re-enfileirada como `pending` |
| 429 | Rate limit por IP/conta | sleep 15 s + `GetNextClient()` | re-enfileirada como `pending` |
| outros | Erro transitório | retorna `nil` | re-enfileirada como `pending` |
A tarefa nunca é descartada: em qualquer falha, `processTask` chama `updateTaskStatus(prefix, "pending")` antes de retornar. Na próxima iteração de qualquer worker disponível, ela será retomada.
**Arquivo:** `crawler/auth_proxy.go`
`IdentityManager` centraliza autenticação, User-Agents e clientes HTTP:
- Carrega contas Docker Hub de `accounts.json` (`[{username, password}]`)
- Atribui `UserAgent` exclusivo a cada conta no carregamento — round-robin sobre `globalUAPool` (7 strings representando Chrome 121, Edge, Firefox 122, Safari 17 em Windows, Mac e Linux)
- `GetNextClient()` retorna `(*http.Client, token, ua)`: o UA é retornado junto com o token para ser propagado coerentemente em todas as requisições daquela identidade
- Login JWT via `POST /v2/users/login/` protegido por `loginMu sync.Mutex` — evita que dois workers loguem a mesma conta simultaneamente
- `ClearToken(token string)` percorre as contas e zera o campo `Token` da conta correspondente ao token expirado; na próxima chamada a `GetNextClient`, `LoginDockerHub` é invocado automaticamente
- O `http.Transport` por cliente configura `MaxIdleConns=100`, `IdleConnTimeout=90s` e `TLSHandshakeTimeout=10s`, mantendo um pool TCP estável e evitando a abertura massiva de sockets (sinal de bot)
### 4.2 新增 `buildgraph/from_mongo.go` 文件
Reengenharia completa do estágio `build` para operação distribuída com claim atômico MongoDB:
```
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 atômico:** cada `repoWorker` usa `ClaimNextBuildRepo` em vez de cursor compartilhado, habilitando execução distribuída em múltiplas máquinas. `ResetStaleBuildClaims` no startup libera claims órfãos de runs anteriores.
**Checkpointing:** `defer MarkRepoGraphBuilt` em `processRepo` garante que `graph_built_at` seja gravado para todos os repositórios processados, inclusive aqueles sem tags disponíveis — eliminando o reprocessamento infinito de repositórios vazios.
### 4.3 修改 `myutils/urls.go`
Template e função para a 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
```
O parâmetro `ordering=-pull_count` foi removido. O Docker Hub utiliza `best_match` como modo padrão de ordenação, que prioriza correspondências exatas de prefixo antes de resultados por popularidade. Para o DFS por prefixo, `best_match` é semanticamente superior: `query="ngin"` retorna `nginx` antes de repositórios que apenas mencionam "nginx" em descrições, maximizando a relevância dos resultados coletados em cada nó da árvore DFS.
A consistência entre páginas é garantida pelo índice único MongoDB em `{namespace, name}`, não pela ordem de chegada.
O upstream declarava o subcomando `crawl` como stub sem utilizar nenhuma API de busca.
### 4.4 新增 `myutils/hubclient.go` 文件
`HubClient` é o cliente HTTP autenticado compartilhado pelos Estágios I e II, eliminando duplicação de código:
- **Interface `IdentityProvider`** — abstração sobre `IdentityManager`; permite que `myutils` não dependa de `crawler`
- **`NewHubClient(ip IdentityProvider) *HubClient`** — uma instância por goroutine
- **`Get(url)`** — 3 tentativas com rotação em 401/429/403; headers Chrome 145 injetados automaticamente
- **`GetInto(url, dest)`** — `Get` + unmarshal JSON
- **`GetTags(ns, name, pageNum, size)`** — busca paginada de tags autenticada
- **`GetImages(ns, name, tag)`** — busca de manifests de imagem autenticada
- **`setHeaders(req)`** — injeta `Accept-Language: pt-BR,pt;q=0.9,...`, `Referer: https://hub.docker.com/`, `Sec-Fetch-*`
- **`rotate()`** — troca identidade internamente via `IdentityProvider`
### 4.5 新增 `buildgraph/metrics.go` 文件
`BuildMetrics` fornece rastreamento de progresso em tempo real para o Estágio II:
- Contadores atômicos para `Processed`, cache hits/misses de tags e imagens, inserções Neo4j, erros
- `newBuildMetrics(threshold)` captura o total de repositórios pendentes no momento do startup
- `startReporter(dataDir, done)` loga e persiste em `build_metrics.log` a cada 60s
- ETA calculado após 30s: `taxa = processed/elapsed_min`, `ETA = (total−processed)/taxa`
### 4.6 修改 `myutils/mongo.go`
Adicionadas ao cliente MongoDB para suportar o crawler de alta vazão e o Estágio II distribuído:
- **`BulkUpsertRepositories(repos []*Repository)`** — bulk write atômico e não-ordenado; ~10-50× mais rápido que upserts individuais em loop para processar uma página de resultados inteira de uma vez
- **`KeywordsColl`** — nova coleção `crawler_keywords` para checkpointing do Estágio I: ao reiniciar, keywords já completamente crawleadas são ignoradas em O(1)
- **`IsKeywordCrawled(keyword)` / `MarkKeywordCrawled(keyword)`** — interface de leitura/gravação do checkpoint do Estágio I
- **`MarkRepoGraphBuilt(namespace, name)`** — grava `graph_built_at` e remove `build_claimed`/`build_started_at` (checkpoint Stage II)
- **`ClaimNextBuildRepo(threshold)`** — `FindOneAndUpdate` atômico para claim de repositório no Stage II
-`ResetStaleBuildClaims()`** — libera claims órfãos no startup do Stage II
- **`CountPendingBuildRepos(threshold)`** — verifica fila vazia para immortal worker pattern
- **`FindImagesByDigests(digests)`** — query em lote com `$in`; substitui N queries individuais
- **Connection pool**: `SetMaxPoolSize(100)`, `SetMinPoolSize(5)`, `SetMaxConnIdleTime(5m)` — estabilidade sob carga paralela alta
- **Timeout do ping inicial**: aumentado de `1s` para `30s` — evita falso-negativo em conexões lentas
### 4.7 修改 `myutils/neo4j.go`
`InsertImageToNeo4j` foi reescrito para **transação única por imagem** (antes: uma transação por layer):
1. Todos os IDs de layer são computados localmente via SHA256 (puro CPU, zero I/O de rede)
2. Toda a cadeia de layers + tag de imagem é inserida em **uma única `ExecuteWrite`** — O(1) round-trips por imagem independente do número de layers
Resultado: latência de inserção cai de O(N layers × RTT) para O(1 × RTT).
**Correção em `findLayerNodesByRawLayerDigestFunc`:** a query original usava `{id: $digest}` para matchar um nó `RawLayer`, mas a propriedade armazenada é `digest`. Corrigido para `{digest: $digest}`. O bug quebrava silenciosamente o rastreamento de imagens upstream.
### 4.8 修改 `myutils/docker_hub_api_requests.go`
O cliente HTTP global foi reestruturado:
- **`DisableKeepAlives: true` removido** — keep-alives habilitados; conexões TCP são reutilizadas entre requisições (economia de ~100-300ms de handshake+TLS por requisição)
- **Connection pool**: `MaxIdleConns: 300`, `MaxIdleConnsPerHost: 50`, `IdleConnTimeout: 90s`
- **`Timeout: 30s`** adicionado ao cliente global
### 4.9 修改 `myutils/config.go`
- **Env vars de override**: `MONGO_URI` e `NEO4J_URI` sobrescrevem os valores do `config.yaml` — permite rodar Node 2 apontando para o MongoDB do Node 1 sem alterar o arquivo de configuração
- **Localização do config**: `filepath.Dir(os.Args[0])` → `os.Getwd()` — o config é buscado relativo ao diretório de trabalho, não ao binário (compatível com `go run`)
- **Neo4j opcional**: se a conexão Neo4j falhar na inicialização, o sistema não aborta — útil para rodar apenas o Estágio I sem Neo4j ativo
### 4.10 Docker Compose 基础设施
Infraestrutura completa para rodar a pipeline:
| Serviço | Imagem | Porta | Propósito |
|---------|--------|-------|-----------|
| `ditector_mongo` | `mongo:latest` | 27017 | Persistência de repos, tags, images |
| `ditector_neo4j` | `neo4j:latest` | 7474/7687 | Grafo IDEA de dependências |
| `ditector_crawler` | `golang:1.22` | — | Executa o crawl com seed configurável |
A variável de ambiente `SEED` permite rodar múltiplas instâncias do crawler com sementes diferentes (estratégia meet-in-the-middle):
```
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` define o serviço `builder` para o Nó 3 (Stage II):
```
DB_HOST= NEO4J_URI=neo4j://:7687 make start-build
```
O volume do Neo4j foi migrado de named Docker volume para host path `./neo4j_data:/data`, protegendo dados contra `docker system prune -a --volumes`.
### 4.11 修改 `scripts/calculate_node_dependent_weights.go`
O branch `if repoDoc.Namespace == "library"` continha `continue` como primeira instrução, tornando todo o código abaixo inalcançável. Imagens oficiais Docker (`library/`) eram silenciosamente ignoradas no cálculo de dependency weight. O `continue` foi removido.
### 4.12 新增自动化脚本(`automation/`)
- `pipeline_autopilot.sh` — executa os 3 estágios sequencialmente com configuração parametrizada
- `test_e2e.sh` — teste de integração end-to-end: crawl com seed `nginx`, build, rank, verifica output
## 5. 先决条件与配置
### 所需软件
```
# Go 1.21+
go version
# Docker 与 Docker Compose
docker --version
docker compose version
```
### 基础设施
Suba MongoDB e Neo4j antes de qualquer comando:
```
docker compose up -d mongodb neo4j
```
Aguarde ~10s para os serviços iniciarem. Verifique:
```
# MongoDB
mongosh localhost:27017 --eval "db.runCommand({ping: 1})"
# Neo4j
curl -s http://localhost:7474 | head -5
```
### Docker Hub 账户(爬取必需)
Crie `accounts.json` na raiz do projeto (NÃO commitar):
```
[
{"username": "usuario1", "password": "senha1"},
{"username": "usuario2", "password": "senha2"}
]
```
### 代理(可选)
Crie `proxies.txt` na raiz (uma URL por linha):
```
http://user:pass@proxy1.example.com:8080
http://user:pass@proxy2.example.com:8080
socks5://proxy3.example.com:1080
```
## 6. `config.yaml` 配置
Copie o template e ajuste:
```
cp config_template.yaml config.yaml
```
Campos principais:
```
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. 第一阶段 — 爬取(发现)
### Docker Hub 仓库命名表示
O Docker Hub organiza imagens em dois níveis hierárquicos: `namespace/name`. Não existem namespaces aninhados (diferente do GitHub). A API V2 retorna o campo `repo_name` em dois formatos possíveis:
| Tipo | `repo_name` na API | Namespace real | Nome real |
|------|--------------------|----------------|-----------|
| Imagem oficial (`library`) | `"nginx"` | `library` | `nginx` |
| Imagem oficial (`library`) | `"postgres"` | `library` | `postgres` |
| Imagem community | `"cimg/postgres"` | `cimg` | `postgres` |
| Imagem community | `"redis/redis-stack"` | `redis` | `redis-stack` |
O campo `repo_owner` presente na resposta da API é **sempre vazio** (`""`) para todos os tipos de repositório — não deve ser utilizado. O `namespace` correto é extraído exclusivamente do `repo_name` via `parseRepoName()` em `crawler/crawler.go`:
```
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")
}
```
**Por que isso é crítico para o `docker pull` e o OpenVAS:**
- Imagens `library/`: o namespace pode ser omitido. `docker pull nginx` equivale a `docker pull library/nginx`.
- Imagens community: o namespace é **obrigatório**. `docker pull cimg/postgres` não funciona sem o prefixo `cimg/`. Sem ele, o Docker interpreta como `library/postgres` — imagem diferente, resultado de scan inválido.
O formato correto para gerar o nome de pull a partir do dataset exportado:
```
ns = record["repository_namespace"]
img = record["repository_name"]
tag = record["tag_name"]
# 对于 library 镜像,命名空间在拉取时省略(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
```
**Verificação empírica:** Em amostragem de 1.000 resultados da API V2 cobrindo 10 queries distintas (`nginx`, `redis`, `postgres`, `mysql`, `debian`, `ubuntu`, `python`, `node`, `go`, `java` nenhum `repo_name` apresentou mais de uma barra. O formato `namespace/name` é o teto estrutural do Docker Hub.
### 功能说明
O crawler varre o Docker Hub usando a estratégia **DFS (Depth-First Search)** sobre o espaço de keywords, descobrindo repositórios e persistindo `namespace`, `name` e `pull_count` no MongoDB.
**Fluxo interno:**
```
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
```
### 执行方式
**Modo simples (uma máquina):**
```
go run main.go crawl \
--workers 20 \
--accounts accounts.json \
--config config.yaml
```
**Modo acelerado (múltiplas máquinas / meet-in-the-middle):**
```
# 机器 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
```
**Com proxies:**
```
go run main.go crawl --workers 20 --proxies proxies.txt --accounts accounts.json --config config.yaml
```
### 参数
| Flag | Padrão | Descrição |
|------|--------|-----------|
| `--workers` / `-w` | 10 | Número de goroutines trabalhadoras paralelas |
| `--seed` | — | Keywords iniciais para DFS, separadas por vírgula (sem seed = começa por todo o alfabeto) |
| `--shard` | -1 | Índice do shard (base 0) para crawl distribuído; requer `--shards` |
| `--shards` | 1 | Total de shards para distribuição meet-in-the-middle (ex: 2 para dividir o alfabeto entre 2 máquinas) |
| `--accounts` | — | Caminho para `accounts.json` |
| `--proxies` | — | Caminho para arquivo de proxies (uma URL por linha) |
| `--config` / `-c` | `config.yaml` | Caminho para o arquivo de configuração |
### 查看进度
```
# 仓库发现计数
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()
'
```
### 预期体积
Com 1 máquina e 20 workers rodando por 24h, espera-se descobrir entre 500.000 e 2.000.000 repositórios, dependendo da velocidade da conexão e dos rate limits. O Docker Hub contém 12M+ repositórios no total.
## 8. 第二阶段 — 构建(IDEA 图)
### 功能说明
Para cada repositório no MongoDB com `pull_count >= threshold`, o Estágio II:
1. Reivindica atomicamente o repositório via `ClaimNextBuildRepo` (MongoDB `FindOneAndUpdate`), garantindo que nenhum outro worker o processe simultaneamente
2. Consulta o cache MongoDB de tags; recorre à API Docker Hub com autenticação JWT (HubClient) apenas quando o cache não contém o dado
3. Para cada tag, consulta o cache MongoDB de imagens; acessa a API para obter layers (digest, instruction, size) quando necessário
4. Filtra imagens Windows
5. Insere no Neo4j o grafo IDEA com o algoritmo de hashing de layer IDs (seção 3.2 do paper)
6. Marca o repositório como concluído via `MarkRepoGraphBuilt` (campo `graph_built_at`) — executado via `defer`, portanto garantido inclusive para repositórios com 0 tags
O Stage II pode ser executado em múltiplas máquinas simultaneamente. O claim atômico elimina reprocessamento duplicado sem nenhuma coordenação adicional entre nós.
### 执行方式
**Via Makefile (Nó 3 — recomendado):**
```
# 配置变量并启动构建容器
DB_HOST= NEO4J_URI=neo4j://:7687 make start-build
# 跟踪日志
make logs-build
```
**Via linha de comando (desenvolvimento / teste local):**
```
go run main.go build \
--format mongo \
--threshold 1000 \
--tags 3 \
--accounts accounts.json \
--data_dir /tmp/ditector_build \
--config config.yaml
```
### 参数
| Flag | Padrão | Descrição |
|------|--------|-----------|
| `--format` | `mongo` | Fonte de dados (somente `mongo` suportado) |
| `--threshold` | 1.000.000 | Pull count mínimo para processar um repositório |
| `--tags` | 10 | Número de tags mais recentes a processar por repositório |
| `--accounts` | — | Caminho para `accounts.json` (autenticação JWT — mesmo arquivo do Estágio I) |
| `--proxies` | — | Caminho para arquivo de proxies (opcional) |
| `--data_dir` | `.` | Diretório para `build_checkpoint.jsonl` e `build_metrics.log` |
Os parâmetros `--page` e `--page_size` foram removidos: o controle de progresso é gerenciado pelo campo `graph_built_at` no MongoDB (via claim atômico), não por paginação manual.
**Recomendações para pesquisa:**
- `--threshold 1000` — cobre a maior parte dos repositórios com atividade real
- `--tags 3` — alinhado com o paper Dr. Docker; as 3 tags mais recentes são suficientes para análise de herança
### 进度监控
```
# 实时 ETA 的度量指标
tail -f build_metrics.log
# 度量指标行示例:
# [METRICS 02:15:00] progress=1234/48000 (2.6%) | rate=45.2 repos/min | ETA=17h22m | cache tags=82% imgs=71% | neo4j=12340 | errors=3 | uptime=27m18s
# 已完成的仓库(检查点中的行)
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 数据持久化
O Neo4j persiste em `./neo4j_data/` (host path explícito). Essa pasta é criada automaticamente pelo Docker Compose no primeiro start. Ao contrário de named Docker volumes, ela não é afetada por `docker system prune -a --volumes`. Inclua `neo4j_data/` nos seus backups regulares junto com `mongo_data_secure/`.
## 9. 第三阶段 — 排名(优先级)
### 功能说明
Para cada imagem processada no grafo Neo4j, calcula o **Dependency Weight** (Out-Degree no IDEA) — número de imagens downstream que herdam desta imagem — e exporta um arquivo JSONL com os resultados.
**Schema de saída (um JSON por linha):**
```
{
"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 后处理
Ordene por dependency weight (descrescente) e pull count para priorização:
```
# 按 dependency weight 排序的前 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 集成
O objetivo final da pipeline é alimentar um scanner OpenVAS com containers de rede. O fluxo é:
```
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
```
**Containers sem serviços de rede:** se o container não expõe portas ou não roda um daemon de rede, o OpenVAS não encontrará serviços. O script externo de scanning deve tratar esse caso avançando para o próximo container.
## 11. 流水线自动化
### 自动飞行流水线
Executa os 3 estágios sequencialmente:
```
./automation/pipeline_autopilot.sh "a"
```
Configurações no próprio script:
```
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"
```
### 端到端集成测试
Valida que toda a pipeline funciona end-to-end com dados reais (seed `nginx`):
```
chmod +x automation/test_e2e.sh
./automation/test_e2e.sh
```
O que o teste verifica:
1. Crawl com seed `nginx` por 20s → descobre repositórios relacionados a nginx
2. Build com threshold=0 → processa todos os repositórios descobertos
3. Rank → gera `test_output.json`
4. Verifica que `test_output.json` existe e tem tamanho > 10 bytes
## 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
```
### 全局标志
| Flag | Padrão | Descrição |
|------|--------|-----------|
| `--config` / `-c` | `config.yaml` | Arquivo de config |
| `--log_level` / `-l` | `debug` | Nível de log: debug, info, warn, error, critical |
### `execute --script`
| Script | Descrição |
|--------|-----------|
| `calculate-node-weights` | Calcula Dependency Weight de cada imagem e exporta JSONL |
| `analyze-threshold` | Analisa imagens com pull_count acima de threshold |
| `analyze-all` | Analisa todas as imagens no MongoDB |
| `count-images-with-upstream` | Conta imagens com upstream (In-Degree > 0)
| `count-images-with-downstream` | Conta imagens com downstream (Out-Degree > 0) |
| `export-mongo-result-docs` | Exporta resultados de análise do MongoDB para JSON |
| `check-same-node-as-high-dependent-images` | Identifica interseções entre conjuntos high-PC e high-DW |
## 14. 设计决策与权衡
### 为何在此分支中使用 Go 实现爬虫?
O upstream declarava o subcomando `crawl` em `cmd/cmd.go` sem campo `Run` — registrado mas sem implementação. O Estágio I foi implementado neste fork em Go pela consistência de stack e pelas vantagens para workloads de I/O intensivo:
- **Goroutines**: escala para centenas de workers com ~2KB/goroutine (vs ~1MB/thread OS)
- **Channels**: comunicação entre estágios type-safe sem locks manuais
- **Único binário**: deploy trivial em múltiplas máquinas, sem runtime externo
### 为何构建阶段调用实时 API 而非读取 MongoDB?
O crawler (Estágio I) armazena apenas `namespace`, `name` e `pull_count`. Tags e layers são buscados no Estágio II via API live. Trade-off deliberado:
- **Prós**: volume de dados no MongoDB é menor; o crawler é mais rápido
- **Contras**: o build stage depende da disponibilidade da API; repositórios deletados entre crawl e build geram erros logados
Alternativa não implementada: o crawler poderia armazenar tags/layers diretamente, tornando o build stage totalmente offline.
### 已知限制
1. **JWT expiry e re-login**: ao receber HTTP 401, `fetchPage` chama `ClearToken` para invalidar o token expirado e `GetNextClient` para obter uma nova identidade com login automático. Se todas as contas estiverem simultaneamente com token inválido, o retry pode falhar para a página em questão.
2. **Build live API**: se um repositório for deletado entre o crawl e o build, erros são logados mas não interrompem o progresso.
3. **Throughput do Neo4j**: uma transação por imagem (O(1) round-trips). Para volumes >1M imagens, o gargalo migra para a memória heap do Neo4j — aumentar `NEO4J_dbms_memory_heap_max__size` é recomendado.
*Baseado no paper: Hequan Shi et al., "Dr. Docker: A Large-Scale Security Measurement of Docker Image Ecosystem", WWW '25.*
标签:DITector, Docker, Dr. Docker, EVTX分析, IDEA, OpenVAS, PHP, Pull Count, Web截图, 依赖图, 分布式采集, 大规模测量, 安全优先级, 安全测量, 安全防御评估, 容器安全, 容器生态, 日志审计, 生态测量, 网络暴露, 请求拦截, 风险优先级