SCA, SAST e DAST: como coloquei segurança de verdade na minha pipeline
Durante muito tempo, CI/CD pra mim era só build, teste e deploy. Nenhuma dessas etapas responde se o código é seguro. Implementei SCA, SAST e DAST numa pipeline real e aprendi na prática o que cada um encontra e o que nenhum deles substitui.
Durante muito tempo, quando eu pensava em CI/CD, pensava basicamente nisso:
Se compilou e os testes passaram, beleza.
Só que existe um problema: nenhuma dessas etapas necessariamente responde se o código é seguro.
Uma aplicação pode compilar, passar em todos os testes e continuar vulnerável a SQL Injection, usar uma dependência com CVE conhecida ou até ter um secret commitado no repositório.
Foi estudando segurança de aplicações que comecei a implementar uma pipeline de DevSecOps em um projeto vulnerável que criei justamente para isso.
E três conceitos apareceram o tempo todo:
SCA, SAST e DAST.
Eles parecem fazer a mesma coisa, mas analisam partes completamente diferentes da aplicação.
Primeiro: qual a diferença entre SCA, SAST e DAST?
A forma mais simples que encontrei de pensar neles é:
SCA analisa o que você instalou. SAST analisa o que você escreveu. DAST analisa como sua aplicação se comporta quando alguém tenta quebrá-la.
Em uma pipeline, podemos imaginar:
Eles não são concorrentes.
Um encontra problemas que o outro muitas vezes nem consegue enxergar.
1. SCA — Software Composition Analysis
Hoje praticamente nenhuma aplicação é escrita do zero.
No meu backend Node.js, por exemplo, tenho dependências como Express, bibliotecas para JWT, acesso ao PostgreSQL e várias outras.
O problema é que:
Uma vulnerabilidade não precisa estar no código que eu escrevi.
Ela pode estar em uma biblioteca que instalei.
É exatamente aí que entra o SCA.
No projeto, comecei com:
npm auditO npm audit compara a árvore de dependências do projeto com advisories de vulnerabilidades conhecidas.
E foi assim que encontrei, por exemplo, uma versão vulnerável do jsonwebtoken.
Ou seja: eu poderia revisar meu código inteiro e não encontrar aquele problema.
O problema estava na composição do software.
Na pipeline coloquei:
- name: Audit dependencies
run: npm audit --audit-level=highAgora o CI também verifica as dependências.
E o --audit-level=high começa a introduzir outro conceito importante: security gate.
Eu posso decidir que vulnerabilidades menores serão reportadas, enquanto vulnerabilidades High ou Critical impedem aquela execução da pipeline de passar.
2. SAST — Static Application Security Testing
Depois das dependências, vem o código que nós escrevemos.
Foi aí que adicionei SAST.
A principal característica é que o SAST consegue procurar padrões potencialmente vulneráveis sem precisar atacar uma aplicação rodando.
No meu caso usei duas ferramentas:
- CodeQL
- Semgrep
O CodeQL é particularmente interessante porque transforma o código em uma representação consultável e procura fluxos que correspondem a problemas de segurança.
Na pipeline:
- uses: github/codeql-action/init@v4
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@v4Também adicionei Semgrep:
- name: Install Semgrep
run: pip install semgrep
- name: Run Semgrep
run: semgrep scan --config=autoDurante os testes, o CodeQL conseguiu apontar problemas que passariam facilmente despercebidos em uma revisão rápida, como log injection.
A ideia aqui é diferente do SCA:
Mas ainda existe uma limitação importante.
Estamos analisando o código.
Não estamos necessariamente observando o comportamento real da aplicação enquanto ela está rodando.
É aí que entra a parte que achei mais interessante.
3. DAST — Dynamic Application Security Testing
No DAST eu parei de simplesmente analisar arquivos.
Eu subi a aplicação e deixei uma ferramenta tentar atacá-la.
Usei o OWASP ZAP.
Minha pipeline passou a fazer aproximadamente isso:
O ZAP recebe minha especificação OpenAPI e começa a testar os endpoints.
E aqui apareceu uma diferença enorme em relação ao SAST.
Ele pode fazer requests como:
GET /notes/search?q='
Authorization: Bearer <token>e observar o que acontece.
Em um dos scans, esse request resultou em:
500 Internal Server ErrorO ZAP então levantou um alerta de possível:
Isso não significa automaticamente que toda descoberta do scanner é uma vulnerabilidade confirmada.
Nesse caso, por exemplo, a confiança reportada era baixa. O próximo passo é analisar a implementação, reproduzir o comportamento e determinar se realmente existe SQL Injection.
Essa parte é fundamental:
Scanner encontra indícios. Desenvolvedor ainda precisa entender o finding.
Um problema que encontrei implementando DAST
No começo, o scan parecia funcionar.
Só que quase todas as requisições estavam retornando:
401 UnauthorizedO ZAP estava "testando" minha API, mas não conseguia chegar justamente nos endpoints que eu queria testar.
Foi necessário criar um usuário específico para o DAST durante a execução da pipeline:
Depois apareceu outro problema.
Rate limiting.
Eu tinha adicionado rate limits justamente como proteção de segurança, mas um scanner envia uma quantidade enorme de requests.
Resultado:
O scanner novamente perdia cobertura.
Esse foi um aprendizado importante: não basta colocar uma ferramenta de segurança no CI e assumir que ela está testando sua aplicação corretamente.
Um scan verde com autenticação quebrada pode ser muito pior que um scan vermelho.
Ele passa uma falsa sensação de segurança.
Secret scanning também entrou na pipeline
Além dos três principais, coloquei também o Gitleaks.
O objetivo é diferente:
Um secret encontrado pode significar que aquela credencial já precisa ser considerada comprometida.
No GitHub Actions ficou algo semelhante a:
- uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Isso complementa os outros scanners.
Como ficou a pipeline
Depois de juntar tudo:
Mas ainda faltava uma coisa.
O que acontece quando alguma ferramenta encontra uma vulnerabilidade?
Os findings também precisam chegar em algum lugar
Outra coisa que implementei foi fazer com que os resultados dos scanners não ficassem perdidos no log do GitHub Actions.
No OWASP ZAP, por exemplo, podemos permitir que a própria Action registre os resultados em uma GitHub Issue:
allow_issue_writing: trueAssim, o finding não existe apenas durante aquela execução da pipeline. Ele vira algo que pode ser acompanhado e tratado dentro do próprio repositório.
Com o CodeQL, a integração é ainda mais direta.
Os resultados da análise são enviados para o code scanning do GitHub e aparecem na área Security and quality, onde é possível visualizar os alerts, severidade, arquivo afetado e acompanhar a correção.
Outras ferramentas de análise estática também podem integrar seus resultados ao GitHub através do formato SARIF:
O GitHub interpreta esse arquivo e transforma os resultados em code scanning alerts dentro do repositório.
Então começamos a separar duas responsabilidades importantes:
Porque reportar um problema e impedir que ele chegue em produção são coisas diferentes.
É aí que entram os security gates.
Encontrar uma vulnerabilidade não é suficiente
Um scanner pode simplesmente gerar um relatório e deixar a pipeline continuar.
Para transformar isso em uma política de segurança de verdade, precisamos definir security gates.
Por exemplo:
Se uma vulnerabilidade violar a política:
E existe uma distinção importante aqui.
Pipeline falhar não significa automaticamente que ninguém consegue fazer merge.
O repositório também precisa exigir aquele status check através das regras de proteção da branch.
Então o fluxo completo passa a ser:
É aqui que segurança deixa de ser apenas um relatório que ninguém lê e começa a fazer parte do processo de desenvolvimento.
Nenhuma dessas ferramentas substitui as outras
Essa foi provavelmente a maior conclusão que tirei implementando tudo.
Imagine que eu instale uma biblioteca vulnerável:
SCA pode encontrar.
Agora imagine que eu escreva uma query SQL concatenando input do usuário:
SAST pode encontrar.
Agora imagine que determinado payload provoque um comportamento inesperado somente com banco, autenticação, middleware e aplicação funcionando juntos:
DAST pode encontrar.
E imagine que eu tenha uma falha de autorização onde:
Talvez nenhum scanner encontre de forma confiável.
É aí que entram revisão humana, testes específicos e Threat Modeling.
Segurança não é:
"Instalei o ZAP, agora minha aplicação é segura."É muito mais próximo de:
Cada camada cobre uma parte diferente do problema.
O que mudou na minha forma de olhar CI/CD
Antes eu pensava:
"A pipeline verifica se meu código funciona."
Hoje eu prefiro pensar:
"A pipeline verifica algumas das condições necessárias para eu confiar que esse código pode seguir adiante."
Build passar não significa código correto.
Teste passar não significa código seguro.
SAST passar não significa aplicação segura.
DAST passar também não significa aplicação segura.
Mas quando combinamos várias dessas técnicas, aumentamos bastante nossa capacidade de encontrar problemas antes que eles cheguem em produção.
E, para mim, essa foi a parte mais interessante de estudar DevSecOps na prática:
segurança deixa de ser uma etapa feita no final do projeto e passa a fazer parte do ciclo normal de desenvolvimento.