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截图, 依赖图, 分布式采集, 大规模测量, 安全优先级, 安全测量, 安全防御评估, 容器安全, 容器生态, 日志审计, 生态测量, 网络暴露, 请求拦截, 风险优先级