{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://codafort.dev/schemas/coda-dast-v1.schema.json",
  "title": "coda-dast/1",
  "description": "Envelope da modalidade DAST (emitido pelo codaprobe, scanner outside-in). O nome segue a convenção coda-<modalidade>/1 (ADR-0002): o contrato é nomeado pelo QUE ele carrega, não por qual binário o emite — nome de produto é refém de rebrand, modalidade não. É irmão de codafort (fonte) e codacrash (artefato) sob o guarda-chuva Codafort, e seus findings são o Finding canônico coda-finding/1 com moment=run. Determinístico na emissão (RF-500): a mesma app no mesmo estado produz o mesmo envelope byte-a-byte — é o que torna o scorecard reprodutível e o gate de CI possível.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema",
    "tool",
    "scan",
    "findings"
  ],
  "properties": {
    "schema": {
      "const": "coda-dast/1"
    },
    "tool": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name",
        "version"
      ],
      "description": "Proveniência: quem emitiu e em que versão.",
      "properties": {
        "name": {
          "const": "codaprobe"
        },
        "version": {
          "type": "string",
          "minLength": 1
        }
      }
    },
    "findings": {
      "type": "array",
      "description": "Findings observados, ORDENADOS (I-DETERM). Lista vazia é resultado legítimo — significa 'nada observável de fora', não falha.",
      "items": {
        "$ref": "#/definitions/finding"
      }
    },
    "coverage": {
      "type": "array",
      "description": "O que o scan ativo testou, ORDENADO: uma linha por (método, location, canal, parâmetro), com cada pack de payload em finding, tested_clean ou not_tested. Não testado nunca vira limpo: tested_clean exige que todo payload do pack tenha tido resposta. Ausente no scan só passivo. Cobre os packs do scanner ativo; booleano, temporal, OOB, redirect, CRLF, API e GraphQL ainda não entram.",
      "items": {
        "$ref": "#/definitions/coverage"
      }
    },
    "meta": {
      "type": "object",
      "description": "Metadados determinísticos do run (chave→valor, ordenados). Nunca carrega segredo do alvo nem conteúdo de resposta.",
      "additionalProperties": {
        "type": "string"
      },
      "properties": {
        "requests": {
          "type": "string",
          "description": "Quantidade de decisões de escopo registradas no audit log."
        },
        "audit_anchor": {
          "type": "string",
          "description": "SHA-256 final da cadeia do audit log (RF-580). Guardado junto do laudo, prova depois que nenhuma decisão foi alterada, removida ou reordenada — verificável por `codaprobe verify-audit`."
        }
      }
    },
    "scan": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "target",
        "authorization",
        "scope",
        "started_at_unix",
        "finished_at_unix"
      ],
      "description": "O QUE foi escaneado e sob QUAL autorização (RF-500). Sem este bloco dois laudos de aplicações diferentes são indistinguíveis, e o artefato não se sustenta como evidência. Tudo aqui é estável entre execuções EXCETO os dois carimbos de tempo — a única parte volátil do laudo.",
      "properties": {
        "target": {
          "type": "string",
          "description": "URL-raiz do alvo, redigida (sem userinfo, sem query)."
        },
        "authorization": {
          "type": "string",
          "description": "Atestado declarado pelo operador, copiado do arquivo de escopo. É o registro de QUEM autorizou."
        },
        "scope": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Resumo legível do escopo efetivo, uma linha por entrada. Nunca carrega credencial: a auth vive em arquivo/env próprio, fora do escopo."
        },
        "started_at_unix": {
          "type": "integer",
          "minimum": 0,
          "description": "Início, epoch Unix em segundos (UTC). Epoch de propósito: inequívoco e sem fuso."
        },
        "finished_at_unix": {
          "type": "integer",
          "minimum": 0,
          "description": "Fim, epoch Unix em segundos (UTC)."
        }
      }
    }
  },
  "definitions": {
    "coverage": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "method",
        "location",
        "channel",
        "param"
      ],
      "properties": {
        "method": {
          "type": "string",
          "minLength": 1
        },
        "location": {
          "type": "string",
          "minLength": 1,
          "description": "URL redigida, a mesma location dos findings do endpoint."
        },
        "channel": {
          "type": "string",
          "minLength": 1,
          "description": "Canal da injeção: query parameter, header, cookie, formparam, JSON field, path-param ou parameter NAME."
        },
        "param": {
          "type": "string"
        },
        "finding": {
          "type": "array",
          "description": "Packs que confirmaram finding.",
          "items": {
            "type": "string"
          }
        },
        "tested_clean": {
          "type": "array",
          "description": "Packs com todos os payloads respondidos e nenhum finding.",
          "items": {
            "type": "string"
          }
        },
        "not_tested": {
          "type": "object",
          "description": "Pack → motivo: rótulo do escopo (fora do escopo, método mutante sem opt-in…), erro de transporte, payloads sem resposta ou ponto de injeção ausente.",
          "additionalProperties": {
            "type": "string"
          }
        }
      }
    },
    "finding": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "rule",
        "location",
        "severity",
        "moment"
      ],
      "properties": {
        "rule": {
          "type": "string",
          "minLength": 1,
          "description": "ID estável da regra. SD-PASSIVE-* (observação da resposta), SD-ACTIVE-* (injeção confirmada por oráculo diferencial), SD-API-* (BOLA/BFLA/mass-assignment), SD-CORRELATED-CWE<n> (triangulado com o momento src)."
        },
        "location": {
          "type": "string",
          "minLength": 1,
          "description": "URL onde o sinal foi observado, SEMPRE redigida (I-NOSECRET): sem userinfo e sem query — é onde tokens viajam. URL não-parseável vira placeholder, nunca ecoa a string crua."
        },
        "severity": {
          "enum": [
            "critical",
            "high",
            "medium",
            "low"
          ],
          "description": "Escala canônica do coda-finding/1."
        },
        "moment": {
          "const": "run",
          "description": "Sempre run: o codaprobe só emite evidência observada em execução."
        },
        "evidence": {
          "type": "string",
          "description": "O que foi observado, em pt-BR, curto e sem segredo do alvo. É a frase que justifica o finding a um humano (ex.: qual assinatura surgiu na resposta mutada e estava ausente na baseline)."
        },
        "correlation": {
          "type": "array",
          "description": "Proveniência cross-momento (RF-560): refs às evidências run:/src: que triangulam este finding. Vazio na maioria; preenchido nos SD-CORRELATED-*, que são o atestado forte — o sink existe E é alcançável de fora.",
          "items": {
            "type": "string"
          }
        },
        "confidence": {
          "enum": [
            "confirmed",
            "candidate"
          ],
          "description": "Força da prova. `confirmed`: oráculo diferencial com controle negativo e precisão medida. `candidate`: sinal observado que ainda não é prova e precisa de revisão humana — é o que a família de detectores de API usa enquanto os oráculos não forem refeitos e medidos. Ausente equivale a `confirmed`."
        },
        "confirmation": {
          "enum": ["candidate", "dynamically_confirmed", "exploit_demonstrated"],
          "description": "Degrau da escada de confirmação do coda-finding/1 (PC-1.1 do plan-progressive-confirmation). `candidate` acompanha o `confidence: candidate`; `dynamically_confirmed` é o oráculo que reproduziu; `exploit_demonstrated` é execução controlada e benigna demonstrada (marcador ecoado do cmdi, aritmética do SSTI, callback OOB com nonce)."
        },
        "exchanges": {
          "type": "array",
          "description": "As trocas do audit log que provam o finding: SHA-256 de método, URL redigida e rótulo (canal, parâmetro e papel: baseline, payload n do pack, controle). Sem segredo. O `codaprobe verify-audit --report` confere que cada uma está no log como Allowed. Ausente nos findings passivos, que observam a resposta sem mutação a citar. O SD-CORRELATED-* cita as trocas do finding dinâmico de origem.",
          "items": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          }
        }
      }
    }
  }
}