Web app (Pod + Service no Kubernetes) que escaneia todas as imagens de contêiner rodando no cluster em busca de CVEs, usando syft e grype como bibliotecas Go embutidas (nenhum binário externo é instalado) e o client-go oficial para falar com a API do cluster via kubernetes.default.svc.
O resultado é apresentado em uma SPA (Single-Page Application) que permite filtrar os CVEs pelos nomes dos artefatos (pacotes) encontrados pelo syft, por severidade e por ID de CVE.
Important
O comando deve ser executado a partir de um local que tenha acesso tanto a API do cluster através do kubeconfig, quanto dos registros de imagens de containers que executam no cluster.
As credenciais de cada registro são coletadas durante a execução, baseado no campo imagePullSecrets de cada Pod, ou através de IRSA para clusters EKS.
Baixe o ultimo binário em release e execute o scan:
$ tar xzvf artifactor_0.0.1_linux_arm64.tar.gz artifactor
$ ./artifactor --export my-cluster
$ xdg-open my-cluster/scan.html
Tip
Se o chrome estiver quebrando, use o firefox. Relatórios grandes (>=1GB total) podem não ser carregados devidamente no chrome.
Outra opção é fazer o scan limitado a um ou mais namespaces usando a flag --namespace ns1,ns2,....
O diretorio my-cluster é criado com o relatorio estático e auto-contido do scan completo. Basta abrir o arquivo scan.html gerado.
Os arquivos usados na análise também estão disponíveis nos diretórios sbom/ e grype/.
Tip
Arquivos SBOM podem ser usados posteriormente para scan usando diretamente o grype com o comando grype path/to/file.sbom.
Abaixo está a arvore de arquivos criadas com o scan apenas dos namespaces flux-system e metallb-system:
$ ./artifactor --namespace flux-system,metallb-system --export my-cluster
$ tree my-cluster/
my-cluster/
├── grype
│ ├── ghcr.io_fluxcd_helm-controller_v1.1.0.grype.json
│ ├── ghcr.io_fluxcd_image-automation-controller_v0.39.0.grype.json
│ ├── ghcr.io_fluxcd_image-reflector-controller_v0.33.0.grype.json
│ ├── ghcr.io_fluxcd_kustomize-controller_v1.4.0.grype.json
│ ├── ghcr.io_fluxcd_notification-controller_v1.4.0.grype.json
│ ├── ghcr.io_fluxcd_source-controller_v1.4.1.grype.json
│ ├── quay.io_frrouting_frr_10.4.3.grype.json
│ ├── quay.io_metallb_controller_v0.16.1.grype.json
│ ├── quay.io_metallb_frr-k8s_v0.0.25.grype.json
│ └── quay.io_metallb_speaker_v0.16.1.grype.json
├── sbom
│ ├── ghcr.io_fluxcd_helm-controller_v1.1.0.sbom.json
│ ├── ghcr.io_fluxcd_image-automation-controller_v0.39.0.sbom.json
│ ├── ghcr.io_fluxcd_image-reflector-controller_v0.33.0.sbom.json
│ ├── ghcr.io_fluxcd_kustomize-controller_v1.4.0.sbom.json
│ ├── ghcr.io_fluxcd_notification-controller_v1.4.0.sbom.json
│ ├── ghcr.io_fluxcd_source-controller_v1.4.1.sbom.json
│ ├── quay.io_frrouting_frr_10.4.3.sbom.json
│ ├── quay.io_metallb_controller_v0.16.1.sbom.json
│ ├── quay.io_metallb_frr-k8s_v0.0.25.sbom.json
│ └── quay.io_metallb_speaker_v0.16.1.sbom.json
└── scan.html
3 directories, 21 files
- Descoberta (client-go): lista os Pods de todos os namespaces e coleta as imagens de
containers,initContainerseephemeralContainers, registrando também o namespace e o controller (Deployment/StatefulSet/DaemonSet/Job…) dono de cada Pod (via ownerReferences + ReplicaSets). - imagePullSecrets: para cada Pod, resolve
spec.imagePullSecretse osimagePullSecretsdo ServiceAccount do Pod. Os payloads.dockerconfigjson/.dockercfgsão decodificados e as credenciais são mapeadas para o registry de cada imagem (ex.:index.docker.io,ghcr.io). As credenciais são injetadas no pull feito pelo syft viaimage.RegistryOptions. - SBOM (syft):
pkg.Provide(...)gera o SBOM da imagem — o syft baixa a imagem diretamente do registry usando as credenciais do passo 2. - CVEs (grype): o banco de vulnerabilidades do grype (v6) é baixado/atualizado na inicialização e os pacotes do SBOM são casados contra ele com
VulnerabilityMatcher.FindMatchesContext. - API + SPA: resultados ficam em memória e são servidos via API JSON +
index.html(embutido no binário).
Atenção: o scan puxa as imagens do registry (não do runtime do nó). O Pod precisa de acesso de rede aos registries — imagens em registries inacessíveis aparecem com status
errorna UI.
main.go wiring: flags, k8s client, DB, servidor HTTP, loop de scan
internal/k8s/ descoberta de imagens + resolução de imagePullSecrets
internal/registryauth/ parse do docker config JSON -> credenciais por registry
internal/scanner/ syft (SBOM) + grype (CVE) como bibliotecas; store em memória
internal/api/ API JSON + serve o SPA
web/index.html SPA (vanilla JS, sem build)
deploy/ namespace, SA, ClusterRole(Binding), Deployment, Service
Dockerfile / Makefile build da imagem e automação
| Método | Rota | Descrição |
|---|---|---|
| GET | /api/status |
modo do client k8s, estado do banco grype, se um scan está rodando |
| GET | /api/images |
resumo por imagem: status, contagem de CVEs por severidade |
| GET | /api/cves?image=&artifact=&severity=&exclude=&id=&fixed=&namespace= |
CVEs filtrados (artifact = substring do nome do pacote syft) |
| GET | /api/namespaces |
namespaces distintos dos workloads das imagens escaneadas |
| GET | /api/artifacts?image=&q= |
nomes distintos de artefatos (pacotes) encontrados pelo syft |
| POST | /api/scan |
dispara um rescan de todas as imagens |
| GET | /api/export |
baixa o relatório como um único HTML auto-contido (dados embutidos; funciona offline) |
| GET | /healthz |
liveness/readiness |
| Flag | Env | Default | Descrição |
|---|---|---|---|
--port |
PORT |
8080 |
porta HTTP |
--namespace |
NAMESPACE |
(todos) | restringe o scan a uma lista de namespaces separados por vírgula (--namespace a,b); vazio = todos |
--scan-interval |
SCAN_INTERVAL |
6h |
intervalo de rescan automático (0 desliga) |
--concurrency |
CONCURRENCY |
3 |
scans paralelos |
--db-dir |
DB_DIR |
cache do usuário | diretório do banco de vulnerabilidades do grype |
--db-auto-update |
DB_AUTO_UPDATE |
true |
baixa/atualiza o banco grype na inicialização |
--grype-db-direct-download |
GRYPE_DB_DIRECT_DOWNLOAD |
false |
baixa o banco grype e seus metadados (latest.json + arquivo) diretamente via HTTP(S), sem a biblioteca de download do grype; respeita --insecure-skip-tls e --db-ca-cert |
--ignore-grype-db-unavailable |
IGNORE_GRYPE_DB_UNAVAILABLE |
false |
continua servindo quando o banco grype não está disponível e não pôde ser baixado (os scans falham até ele ficar disponível); sem a flag, o artifactor encerra |
--in-cluster |
IN_CLUSTER |
false |
executa o scan dentro do cluster em um pod temporário com os mesmos parâmetros (requer --export <dir>); o relatório é empacotado em .tar.gz no pod, baixado via API do Kubernetes e extraído localmente |
--in-cluster-force |
IN_CLUSTER_FORCE |
false |
com --in-cluster: sobrescreve recursos existentes e remove tudo no fim sem perguntar |
--in-cluster-image |
IN_CLUSTER_IMAGE |
imagem do deployment | imagem do pod de scan (a imagem precisa conter o artifactor com suporte a --export-tar e ao subcomando interno cat) |
--image-pull-policy |
IMAGE_PULL_POLICY |
IfNotPresent |
imagePullPolicy do pod do --in-cluster (Always, IfNotPresent ou Never) |
--openshift |
OPENSHIFT |
false |
com --in-cluster: aplica os manifests OpenShift/OKD (SecurityContextConstraints + RBAC) que permitem o pod de scan rodar (requer cluster-admin) |
--platform |
PLATFORM |
linux/<GOARCH> |
plataforma da imagem a escanear |
--scan-timeout |
SCAN_TIMEOUT |
15m |
timeout por imagem |
--export-html <arquivo> |
EXPORT_HTML |
— | escaneia uma vez, exporta o relatório em um único HTML auto-contido e sai |
--export-sbom <dir> |
EXPORT_SBOM |
— | escaneia uma vez, exporta um SBOM syft (syft-json) por imagem no diretório e sai |
--export-grype <dir> |
EXPORT_GRYPE |
— | escaneia uma vez, exporta um relatório grype (JSON) por imagem no diretório e sai |
--export <dir> |
EXPORT |
— | escaneia uma vez, exporta o relatório completo (scan.html + sbom/ + grype/) no diretório e sai |
make build # binário local
make run # dev local (usa ~/.kube/config ou KUBECONFIG)
make docker-build # imagens multi-arch (linux/amd64 + arm64) via GoReleaser, sem push (snapshot)
make deploy # aplica os manifests (ns artifactor + RBAC + Deployment + Service)
make port-forward # kubectl port-forward svc/artifactor 8080:8080
make logs # logs do PodDepois do port-forward, abra http://localhost:8080.
- Pela UI: o botão Export no cabeçalho baixa o relatório como um único HTML auto-contido (snapshot dos dados embutido — os filtros funcionam offline, sem o servidor).
- Pela CLI (escaneia uma vez e sai):
artifactor --export-html report.html— só o HTML auto-contidoartifactor --export-sbom sboms/— um SBOM syft (syft-json) por imagemartifactor --export-grype grype/— um relatório grype (JSON) por imagemartifactor --export export/— os três:scan.html+sbom/+grype/- ou
make export(equivale ao--export export/)
Quando a máquina local não alcança os registries (ou o banco grype), o scan pode rodar dentro do cluster com os mesmos parâmetros:
artifactor scan --in-cluster --export relatorio/ --customer-name acme --grype-db-direct-downloadFluxo:
- O artifactor cria um pod
artifactor-scanno namespaceartifactor(junto com namespace/SA/RBAC quando ainda não existem) rodando o scan com os mesmos parâmetros — exceto--exporte--db-dir, reescritos para caminhos dentro do pod. Com--db-ca-cert, o certificado é copiado para o pod via ConfigMap (API do Kubernetes). Em clusters OpenShift/OKD, passe também--openshiftpara aplicar oSecurityContextConstraints+ RBAC que permitem o pod rodar. - O pod executa o scan e compacta todo o diretório de
--exportem um.tar.gz(flag interna--export-tar). - Os logs do pod (stdout + stderr, fundidos pelo runtime) são exibidos no terminal local conforme produzidos (polling de 2s via API do Kubernetes, com
SinceTimepara não repetir linhas); o processo baixa o.tar.gzvia API do Kubernetes (execartifactor cat) e extrai no diretório local de--export(o.tar.gzfica em<dir>.tar.gz). Com--open, oscan.htmlbaixado é aberto localmente. - No fim, o pod e o ConfigMap da CA são removidos — perguntando antes (use
--in-cluster-forcepara sobrescrever recursos existentes e remover sem perguntar).
A imagem é construída e publicada pelo GoReleaser (.goreleaser.yaml): binário estático linux/amd64 + arm64 e manifest multi-arch no ghcr.io (o workflow do GitHub Actions roda a cada push no main e em tags v*). Localmente: make docker-build (snapshot, sem push) ou, para publicar, crie uma tag e rode make docker-push com login no registry.
Para usar sua própria imagem, ajuste deploy/deployment.yaml ou:
kubectl -n artifactor set image deployment/artifactor scanner=ghcr.io/getupcloud/artifactor:0.0.1Para desenvolver, há uma sessão de desenvolvimento em container
(make build-session / make start-session): um ambiente completo que roda
como o usuário local (mesmo uid/gid — os arquivos gravados no repo montado
mantêm o dono correto) e sem dependências do toolchain do host.
make build-session # constrói a imagem artifactor-session a partir do Dockerfile.session
make start-session # inicia uma sessão DeepSeek Coder com o SESSION.md como contexto inicialA imagem (Dockerfile.session) já embarca tudo o que o desenvolvimento precisa:
- Go, make + gcc, git, bash, curl, jq;
kubectl(versão alinhada ao client-go do repo) e o CLIgrype(mesma versão da lib embutida nogo.mod);- python3 + pip e o plugin
security-guidance(carregado pela sessão via--plugin-dir); - o CLI DeepSeek Code (instalado na imagem, exposto como
deepseek).
make start-session monta o diretório atual em /workspace, injeta a
DEEPSEEK_API_KEY (lida do 1Password via op) e inicia o deepseek usando o
SESSION.md como bootstrap — que aponta para AGENTS.md, docs/adr/ e
specs/ como contexto do projeto. O container é descartável (--rm): o estado
vive no repo montado, não na imagem.
Variáveis: SESSION_IMAGE (nome da imagem), DEEPSEEK (comando do CLI) e
BUILD_SESSION_EXTRA_PARAMS (parâmetros extras para o docker build).
Os workflows autenticam no ghcr.io preferindo o secret GHCR_PAT (PAT com scope write:packages) e usando GITHUB_TOKEN como fallback. Se o push falhar com denied: permission_denied: read_package usando o GITHUB_TOKEN, uma destas opções resolve:
- Adicionar o secret
GHCR_PATno repositório (recomendado — mesmo PAT do imagePullSecretdocker-ghcr, desde que tenhawrite:packages). - Vincular o pacote ao repositório: GitHub → conta/org
getupcloud→ Packages →artifactor→ Package settings → Manage Actions access → adicionar o repo com papel Write. - Recriar o pacote com Actions: se o pacote foi criado por push local com outro token, apague-o no UI do ghcr.io e deixe o workflow recriá-lo.
- Habilitar, na org, Packages → permissões de criação/accesso para GitHub Actions.
O workflow release (disparado manualmente na aba Actions) lê a variável VERSION do Makefile, falha se a tag v<VERSION> já existir (local ou remoto), cria e publica a tag e roda o GoReleaser: imagens multi-arch no ghcr.io (:<VERSION> e :<sha> sempre; :latest apenas quando a versão é a mais nova entre as tags v*) + GitHub Release com os binários. Para lançar uma nova versão, edite VERSION no Makefile e dispare o workflow.
O scanner usa uma ServiceAccount dedicada com permissões mínimas (ClusterRole):
pods (list/get/watch), replicasets (list/get — para resolver a cadeia de owners Pod → ReplicaSet → Deployment e exibir o controller de cada imagem), serviceaccounts (get) e secrets (get) — estes dois últimos apenas para resolver os imagePullSecrets referenciados pelos Pods/SAs.
- Imagens hospedadas em registries sem rota de rede a partir do Pod do scanner ficam com status
error. - O primeiro download do banco de vulnerabilidades do grype exige acesso à internet (ou forneça um banco já existente via
--db-dir+--db-auto-update=false). Se não houver banco utilizável e o download falhar, o processo encerra (use--ignore-grype-db-unavailablepara continuar servindo mesmo assim). - Credenciais de
credHelpers/credStore(clientes docker externos) não se aplicam; apenas o conteúdo dos secrets é usado.