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 que permite filtrar os CVEs pelos nomes dos artefatos (pacotes) encontrados pelo syft, por severidade e por ID de CVE.
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
O diretorio my-cluster é criado contendo o relatorio estatico e auto-contido do scan completo. Basta enviar ao cliente ou usar para analisar as imagens;
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 |
--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/)
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.1Os 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). - Credenciais de
credHelpers/credStore(clientes docker externos) não se aplicam; apenas o conteúdo dos secrets é usado.