O formato segue Keep a Changelog e SemVer.
- Nova checagem
payload-duplicate-key(HIGH · A07:2025 · CWE-347): chave repetida no payload, simétrica àheader-duplicate-keyque já existia.json.loadsfica com a última ocorrência; um parser first-wins fica com a primeira — a divergência é o ataque. Umrole/exp/audduplicado deixa um verificador leruser/expirado e outroadmin/válido a partir do mesmo token. O achado do payload não é menos severo que o do cabeçalho porque é o payload que carrega as claims de autorização e expiração. A checagem de segmento foi generalizada para rodar idêntica nos dois segmentos (cabeçalho e payload). Coberta por caso positivo dedicado, meta-teste de catálogo e teste property-based (Hypothesis) sobre a invariante "qualquer segmento com chave repetida ⇒ pelo menos um achado".
payload-sensitive— descritor de recurso deixa de ser falso-positivo: uma chave-sensível terminada em descritor (_name/_type/_uid/_namespace/_kind/_ref, com.///-normalizados para_) descreve um metadado, não carrega o segredo. O token legado de ServiceAccount do Kubernetes carimbakubernetes.io/serviceaccount/secret.name = admin-user-token-6gl6l(o nome do objeto Secret) e disparava MÉDIA indevida. O gate por valor (assinatura forteAKIA…/ghp_…/sk-…/PEM/webhook) continua valendo sob qualquer chave, para não cegar um segredo real que apareça sob uma chave-descritor.claim-long-lifetime— refresh token reconhecido por convenção de biblioteca: a supressão para refresh token não depende mais do nome do campotyp. Reconhece as convenções mainstream —typ(Keycloak),type(Flask-JWT-Extended) etoken_type(djangorestframework-simplejwt) com valorrefresh(case-insensitive) —, mantendo intocado o gate anti-abuso: só suprime quando não há sinais de access token (scope/scp/azp); um access token de 30 dias comscope: adminnão ganha isenção só por se declararrefresh.- Proveniência:
commitresolvido pelo diretório do pacote, não pelo CWD: o Chaveiro audita um token (não varre um repositório), então ocommitdo envelope é o SHA da própria ferramenta. Antes,git rev-parse HEADrodava sem-C, herdando o diretório de trabalho — rodarchaveirode dentro de outro repositório git carimbava o HEAD daquele repo (silenciosamente errado). Agora o git é consultado no diretório do pacote (Path(__file__).resolve().parent, viagit -C), eCHAVEIRO_COMMITsó é aceito se for um SHA de 40 hex (^[0-9a-f]{40}$) — um valor malformado (HEAD, SHA truncado) é ignorado em vez de carimbado. Invariante travada por teste (revert→vermelho): rodando de dentro de um repositório git qualquer, o commit carimbado é o do pacote, nunca o do CWD.
- README (PT/EN) realinhado: nova linha de catálogo para a classe parser-differential
(
header-duplicate-key/payload-duplicate-key); a nota de falso-positivo dopayload-sensitivevirou calibração de baixo falso-positivo (casamento por token de nome —secretary/discardnão disparam —, descritor de recurso como metadado, e o gate por valor sob qualquer chave), somada à calibração de refresh token doclaim-long-lifetime; e a seção de proveniência e a linhaCHAVEIRO_COMMITpassaram a descrever a resolução do commit pelo diretório do pacote e o gate de 40 hex.
-o/--outputeminspectebatch(paridade com Guardião/Esteira). Grava o laudo direto em UTF-8, evitando a armadilha do operador>do PowerShell (que recodifica linha a linha, lento e sujeito a mojibake no Windows) e servindo de canal limpo paraupload-sarifnum passo de CI. Exige--format json.- Nova checagem
header-zip-jws(HIGH · A06:2025 · CWE-409):zip(compressão) declarado num JWS de 3 segmentos. O parâmetro só é válido em JWE (RFC 7516); num JWS viola o RFC 7515 e é o vetor de DoS por descompressão pré-verificação — a biblioteca que descomprime o payload antes de checar a assinatura (caso Apache James) fica exposta sem o atacante conhecer chave nenhuma. Detectável offline, só lendo o cabeçalho. Coberta por caso positivo e negativo + o meta-teste de catálogo.
- README com prova de campo e Pro honesto: nova seção "Prova de campo" com
os números medidos da bateria real (recall 22/22 vetores, 0 falso-positivo em
6/6 tokens legítimos, 24.606 tokens/s no
batch, token hostil não derruba o lote) e o falso-positivo conhecido (secretcomo substring casasecretary) documentado em vez de escondido. A seção Pro passa a dizer com todas as letras que a engine é a mesma — o Pro é serviço (auditoria guiada, PoC autorizado, validação de referência aplicada ao stack do cliente), não código diferente atrás de paywall.
- Token hostil não derruba mais o lote (
batch): um JWT com payload profundamente aninhado levantavaRecursionError(json.loads/json.dumpssão recursivos), abortava o lote inteiro e apagava a auditoria dos demais — quebrando o contrato documentado de isolamento por token. Agoradecodeaplica um teto explícito de aninhamento (64 níveis, medido sem recursão sobre os bytes), o que protege decodificação, render e serialização de uma vez;audit_batchisola qualquer exceção por token (não sóJWTError), registrando o tipo no campoerror. Nenhum JWT legítimo passa de poucos níveis. - base64url estrito (RFC 7515 §2):
b64url_decoderejeita segmentos com caractere fora do alfabetoA-Za-z0-9-_, com espaço/quebra de linha, ou em base64 padrão (+/). O decodificador era tolerante (herdado dourlsafe_b64decode, que descarta bytes inválidos em silêncio) — um auditor leniente laudava um token que verificador estrito nenhum aceita. exp/nbfnão-numérico agora falha fechado: a referência de validação e o detector passivo tratavam uma claim temporal presente-mas-não-numérica (exp: "1",Infinity) como ausente — um token comexpem string nunca expirava. A referência passa a rejeitar (RFC 7519 §2: NumericDate é número) e o detector emite o novo achadoclaim-malformed-time(Média).claim-long-lifetimedispara semiat: um token de 10 anos semiat(mais perigoso, não menos) escapava porque a checagem exigiaexpeiat. Semiat, a vida útil passa a ser aproximada porexp - agora.payload-sensitivevarre em profundidade: segredo/PII em objeto aninhado ({"user": {"cpf": …}}— a forma mais comum de payload JWT no Brasil) escapava porque o loop só olhava o primeiro nível. Agora percorre dicts/listas aninhados (sem recursão), casa chaves compostas (user_password,db_secret) e detecta CPF de 11 dígitos crus (com validação dos dígitos verificadores, para não gerar falso positivo em telefone/id).- JWT aninhado real (RFC 7519 §5.2): apresentar um JWS-in-JWS legítimo
(payload da casca = outro JWS compacto) fazia o
decodeabortar com "payload não é JSON" — a auditoria inteira parava no exato vetor que a ferramenta diz cobrir. Agora a casca é decodificada, o token interno fica emtoken.nestedepayload-nested-jwté emitido. - Injeção de marcação do Rich no relatório: dado controlado pelo emissor do
token (
alg,kid, claims, mensagem de erro) contendo[/]/[bold]derrubava o console comMarkupErrorou — pior — saía interpretado (cor/hyperlink forjados no laudo). Todo campo externo passa porText, saindo literal. - Removido arquivo de resíduo versionado por engano na raiz do repositório.
- Contrato JSON alinhado à suíte (
schema: "suite-appsec/1"): identificador do achado emid(eracheck),severity_rankpara ordenar entre ferramentas sem tabela de-para,by_severitysempre com as 5 chaves (inclusive zeradas) eowasp_editioncomo campo próprio. Quebra quem consumia a chavecheck. - OWASP Top 10 migrado para a edição 2025 (vigente): o ano vai no cabeçalho
da coluna (
OWASP 2025 / CWE) e no JSON; os códigos passam aA02→A04(Cryptographic Failures),A03→A05(Injection),A10→A01(SSRF absorvido em Broken Access Control). Antes o dado não estava errado, mas incoerente com o resto da suíte. batch: código de saída depende só de--fail-on. Linha malformada é ruído normal de token colhido de log — a contagem vai para o stderr e não derruba mais o build (era exit 2). O2fica reservado a erro de uso. Novo--strictpara quem quiser travar em entrada malformada.--fail-onpassa a aceitarinfo.- Subcomando
rulesrenomeado pararegras(rulescontinua como alias).
- Aviso de autorização em tempo de execução nos comandos ofensivos (
crack,forge,forge-confusion): imprimem o enquadramento legal (Lei 12.737/2012 c/ 14.155/2021) no stderr antes de agir. --wordlistdocracké lida em streaming (linha a linha): consumo de memória O(1) e o primeiro acerto encerra a leitura, em vez de materializar a lista inteira (rockyou ~140 MB carregava centenas de MB antes do 1º palpite).- Portão de cobertura no CI (
--cov-fail-under=90),dependabot.ymle actions do CI fixadas por SHA. - Meta-teste de catálogo: toda checagem do
CATALOGpassa a exigir um caso positivo (e casos negativos que matam a inversão silenciosa de um detector) — uma checagem nova nasce vermelha até ter teste. - Detecção de JWT confusion por aninhamento (checagem passiva, sem rede):
header-cty-nestedsinaliza quando o cabeçalho declaracty: JWT(comparação case-insensitive e com o prefixoapplication/opcional — RFC 7515 §4.1.10 / RFC 7519 §5.2), indicando um token aninhado cuja casca um verificador pode validar sem checar o miolo;payload-nested-jwtsinaliza quando uma claim carrega o que aparenta ser outro JWS compacto (heurística conservadora: 3 segmentos e um cabeçalho base64url que decodifica para um objeto JSON comalg). Ambas com A07 · CWE-347. Fecha o item de roadmap "jwt confusion via cty/nested tokens". - Modo batch (
chaveiro batch): audita vários tokens — um por linha — de um arquivo ou do stdin (-), reusando o mesmo pipeline de auditoria passiva doinspect. Ignora linhas em branco e comentários (#) e remove um prefixoBeareropcional. Reporta cada token e um resumo agregado (console ou JSON,mode: "batch"). Exit-code coerente com--fail-on: 1 se a pior severidade do lote atingir o limiar, 2 se houver token malformado (sem atingir o limiar), 0 caso contrário. Um token malformado é reportado sem interromper o lote. Fecha o item de roadmap "Modo batch". A composição decode + checagens virouaudit.audit_token, fonte única usada porinspectebatch. - Verificação de assinatura PS256/PS384/PS512 (RSASSA-PSS, MGF1 e salt do
tamanho do hash — RFC 7518 §3.5) e EdDSA/Ed25519 (RFC 8037) em
core.jwt.verify_asymmetric, e ambos na allowlist do módulo de referência. Verificação real viacryptography; testes de round-trip provam que a assinatura válida passa e que a adulterada (ou verificada com a chave errada) é rejeitada. Fecha o item de roadmap "PS*/EdDSA na referência".
- Decodificação de JWS compacto e primitivas de assinatura/verificação
(HMAC HS*, RS*/ES* via
cryptography). - 17 checagens passivas:
alg:none, alg ausente/desconhecido,jku/x5u/jwk/x5c,kidinjection,exp/iat/aud/iss, vida útil longa e dado sensível no payload. - Ataques:
crack(dicionário HMAC com lista embutida) eforge-confusion(PoC de confusão de algoritmo RS→HS). - Módulo de referência de validação correta (allowlist de algoritmos, rejeita
none, confere assinatura +exp/nbf+aud/iss). - CLI
chaveiro(inspect,crack,forge-confusion,forge,rules). - Suíte de testes com par RSA real e PoC de confusão reproduzido; mypy strict; CI.